crewly 1.20.249 → 1.20.251
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/config/skills/agent/core/complete-task/SKILL.md +5 -1
- package/config/skills/agent/core/complete-task/execute.sh +30 -15
- package/config/skills/agent/core/complete-task/execute.test.sh +45 -1
- package/dist/backend/backend/src/constants.d.ts +49 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +49 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.controller.js +43 -4
- package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +4 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +2 -2
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts +13 -0
- package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/resource-mode.service.js +36 -0
- package/dist/backend/backend/src/services/agent/resource-mode.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +24 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js +44 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.d.ts +48 -0
- package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.js +46 -0
- package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.d.ts +43 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.js +34 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-stats.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-stats.js +1 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-stats.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js +23 -7
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +5 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts +18 -2
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +67 -8
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +117 -3
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js +304 -19
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/stalled-agent-recovery.d.ts +66 -0
- package/dist/backend/backend/src/services/reconciler/stalled-agent-recovery.d.ts.map +1 -0
- package/dist/backend/backend/src/services/reconciler/stalled-agent-recovery.js +107 -0
- package/dist/backend/backend/src/services/reconciler/stalled-agent-recovery.js.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-channel-members.d.ts +5 -0
- package/dist/backend/backend/src/services/slack/slack-channel-members.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-channel-members.js +4 -0
- package/dist/backend/backend/src/services/slack/slack-channel-members.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +10 -0
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +29 -0
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/trace/trace-metrics.d.ts.map +1 -1
- package/dist/backend/backend/src/services/trace/trace-metrics.js +2 -1
- package/dist/backend/backend/src/services/trace/trace-metrics.js.map +1 -1
- package/dist/backend/backend/src/services/trace/trace.types.d.ts +1 -1
- package/dist/backend/backend/src/services/trace/trace.types.d.ts.map +1 -1
- package/dist/backend/backend/src/services/trace/trace.types.js +2 -0
- package/dist/backend/backend/src/services/trace/trace.types.js.map +1 -1
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts +15 -0
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js +47 -0
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js.map +1 -1
- package/dist/backend/backend/src/types/slack.types.d.ts +7 -0
- package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/slack.types.js.map +1 -1
- package/dist/backend/backend/src/types/v2/index.d.ts +1 -1
- package/dist/backend/backend/src/types/v2/index.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/index.js.map +1 -1
- package/dist/backend/backend/src/types/v2/reconcile.types.d.ts +20 -0
- package/dist/backend/backend/src/types/v2/reconcile.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/reconcile.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +49 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +49 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +2 -2
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts +13 -0
- package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/resource-mode.service.js +36 -0
- package/dist/cli/backend/src/services/agent/resource-mode.service.js.map +1 -1
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +24 -1
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js +44 -1
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
- package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.d.ts +48 -0
- package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.d.ts.map +1 -1
- package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.js +46 -0
- package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-channel-members.d.ts +5 -0
- package/dist/cli/backend/src/services/slack/slack-channel-members.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-channel-members.js +4 -0
- package/dist/cli/backend/src/services/slack/slack-channel-members.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +10 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +29 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/services/trace/trace.types.d.ts +1 -1
- package/dist/cli/backend/src/services/trace/trace.types.d.ts.map +1 -1
- package/dist/cli/backend/src/services/trace/trace.types.js +2 -0
- package/dist/cli/backend/src/services/trace/trace.types.js.map +1 -1
- package/dist/cli/backend/src/types/slack.types.d.ts +7 -0
- package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/slack.types.js.map +1 -1
- package/dist/cli/backend/src/types/v2/index.d.ts +1 -1
- package/dist/cli/backend/src/types/v2/index.d.ts.map +1 -1
- package/dist/cli/backend/src/types/v2/index.js.map +1 -1
- package/dist/cli/backend/src/types/v2/reconcile.types.d.ts +20 -0
- package/dist/cli/backend/src/types/v2/reconcile.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/v2/reconcile.types.js.map +1 -1
- package/frontend/dist/assets/{index-2db17f1e.js → index-83188f0b.js} +2 -2
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -13,9 +13,43 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @module services/reconciler/reconciler.service
|
|
15
15
|
*/
|
|
16
|
-
import type { ReconcileResult, ReconcileCorrection, ReconcilerConfig, ReconcilerStatus, WakeAction, WorkItem, Request, TaskClaim } from '../../types/v2/index.js';
|
|
16
|
+
import type { ReconcileResult, ReconcileCorrection, ReconcilerConfig, ReconcilerStatus, WakeAction, WakeActionResult, WorkItem, Request, TaskClaim } from '../../types/v2/index.js';
|
|
17
17
|
import type { AgentHealth } from './reconcile-rules.js';
|
|
18
18
|
import { type IdleHolderSurfacerDeps } from './idle-holder-surfacer.js';
|
|
19
|
+
/** Wake pacing and stalled-agent thresholds (overridable in tests). */
|
|
20
|
+
export interface WakeTuning {
|
|
21
|
+
/** First wait after a failed wake for one session + WorkItem (ms) */
|
|
22
|
+
failureBackoffBaseMs: number;
|
|
23
|
+
/** Ceiling of the doubling failure backoff (ms) */
|
|
24
|
+
failureBackoffMaxMs: number;
|
|
25
|
+
/** How long a pass waits on a start / rehydrate before treating it as pending (ms) */
|
|
26
|
+
startAwaitMs: number;
|
|
27
|
+
/** Queued work this old marks its target as stalled (ms) */
|
|
28
|
+
stalledQueuedAgeMs: number;
|
|
29
|
+
/** An awake agent counts as idle-not-progressing after this long without activity (ms) */
|
|
30
|
+
stalledIdleMs: number;
|
|
31
|
+
/** One stalled-agent recovery per session at most this often (ms) */
|
|
32
|
+
recoveryCooldownMs: number;
|
|
33
|
+
}
|
|
34
|
+
/** Defaults from the backend constants. */
|
|
35
|
+
export declare const DEFAULT_WAKE_TUNING: Readonly<WakeTuning>;
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a provider's wake answer: `true` / `false` (older providers and
|
|
38
|
+
* tests) or a {@link WakeActionResult}.
|
|
39
|
+
*
|
|
40
|
+
* @param value - What `executeWakeAction` resolved to
|
|
41
|
+
* @returns The outcome with its reason
|
|
42
|
+
*/
|
|
43
|
+
export declare function normalizeWakeResult(value: boolean | WakeActionResult | undefined | null): WakeActionResult;
|
|
44
|
+
/**
|
|
45
|
+
* Backoff after the n-th consecutive failed wake: base doubled per failure,
|
|
46
|
+
* capped.
|
|
47
|
+
*
|
|
48
|
+
* @param failures - Consecutive failures so far (>= 1)
|
|
49
|
+
* @param tuning - Base and ceiling
|
|
50
|
+
* @returns Milliseconds before the next attempt
|
|
51
|
+
*/
|
|
52
|
+
export declare function wakeFailureBackoffMs(failures: number, tuning: Pick<WakeTuning, 'failureBackoffBaseMs' | 'failureBackoffMaxMs'>): number;
|
|
19
53
|
/**
|
|
20
54
|
* Interface for providing data to the Reconciler.
|
|
21
55
|
* Abstracts over the actual storage/service layer so the Reconciler
|
|
@@ -56,8 +90,20 @@ export interface ReconcilerDataProvider {
|
|
|
56
90
|
renewClaim?(claimId: string): Promise<void>;
|
|
57
91
|
/** Get all available (queued, unclaimed) WorkItems from the task pool */
|
|
58
92
|
getAvailablePoolItems?(): Promise<WorkItem[]>;
|
|
59
|
-
/**
|
|
60
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Execute a wake action — rehydrate a suspended agent, start an inactive
|
|
95
|
+
* one, or re-push a brief. `true` / `false` are read as ok / failed; a
|
|
96
|
+
* {@link WakeActionResult} also says pending (queued behind the cap) or
|
|
97
|
+
* skipped (nothing attempted) — see CREW-304.
|
|
98
|
+
*/
|
|
99
|
+
executeWakeAction?(action: WakeAction): Promise<boolean | WakeActionResult>;
|
|
100
|
+
/**
|
|
101
|
+
* Stop a stalled agent's session so the next wake starts it fresh
|
|
102
|
+
* (CREW-303). Optional: without it, idle / hung agents are only reported.
|
|
103
|
+
*/
|
|
104
|
+
parkStalledAgent?(sessionName: string, role: string): Promise<boolean>;
|
|
105
|
+
/** Sessions the claim service reports as hung (optional). */
|
|
106
|
+
getHungAgents?(): string[];
|
|
61
107
|
/** Backfill token usage data on completed WorkItems that have 0 tokens */
|
|
62
108
|
backfillTokenUsage?(): Promise<number>;
|
|
63
109
|
/**
|
|
@@ -98,6 +144,22 @@ export declare class ReconcilerService {
|
|
|
98
144
|
/** Where idle-holder reports go; unset = not reported (see {@link setIdleHolderReporting}). */
|
|
99
145
|
private idleHolderDeps;
|
|
100
146
|
private isRunning;
|
|
147
|
+
/** Wake pacing and stalled-agent thresholds */
|
|
148
|
+
private wakeTuning;
|
|
149
|
+
/**
|
|
150
|
+
* Failed wakes per `session + WorkItem` (CREW-304): the next attempt waits
|
|
151
|
+
* a doubling backoff; cleared when the session wakes or the item leaves
|
|
152
|
+
* the queue.
|
|
153
|
+
*/
|
|
154
|
+
private readonly wakeFailures;
|
|
155
|
+
/** Starts still in flight (pending) per session: not requested again meanwhile */
|
|
156
|
+
private readonly pendingStarts;
|
|
157
|
+
/** Sessions whose current pending wake was already traced (one trace per episode) */
|
|
158
|
+
private readonly pendingTraced;
|
|
159
|
+
/** Last stalled-agent recovery per session (epoch ms) */
|
|
160
|
+
private readonly recoveredAt;
|
|
161
|
+
/** Sessions the last hybrid-wake pass already tried (recovery does not repeat them) */
|
|
162
|
+
private wokenThisPass;
|
|
101
163
|
private totalPasses;
|
|
102
164
|
private totalCorrections;
|
|
103
165
|
/**
|
|
@@ -232,5 +294,57 @@ export declare class ReconcilerService {
|
|
|
232
294
|
* @param result - The ReconcileResult to accumulate wake actions into
|
|
233
295
|
*/
|
|
234
296
|
private runHybridWake;
|
|
297
|
+
/**
|
|
298
|
+
* Override wake pacing and stalled-agent thresholds (tests, tuning).
|
|
299
|
+
*
|
|
300
|
+
* @param tuning - Fields to change
|
|
301
|
+
*/
|
|
302
|
+
setWakeTuning(tuning: Partial<WakeTuning>): void;
|
|
303
|
+
/** Map key of a session + WorkItem failure backoff. */
|
|
304
|
+
private wakeKey;
|
|
305
|
+
/**
|
|
306
|
+
* Run one wake. A start / rehydrate that has not finished within
|
|
307
|
+
* `startAwaitMs` (typically queued behind the running-agent cap) is
|
|
308
|
+
* reported pending and settles in the background, so the pass neither
|
|
309
|
+
* stalls nor asks again (CREW-304).
|
|
310
|
+
*
|
|
311
|
+
* @param action - The wake
|
|
312
|
+
* @returns Its outcome now (pending when still in flight)
|
|
313
|
+
*/
|
|
314
|
+
private attemptWake;
|
|
315
|
+
/**
|
|
316
|
+
* Trace and pace one wake outcome: ok clears the session's backoffs; a
|
|
317
|
+
* failure backs that session + WorkItem off (doubling, capped); pending is
|
|
318
|
+
* traced once per episode; skipped is neither traced nor backed off.
|
|
319
|
+
*
|
|
320
|
+
* @param action - The wake
|
|
321
|
+
* @param res - Its outcome
|
|
322
|
+
* @param result - The pass result to count into (null when it settled after the pass)
|
|
323
|
+
*/
|
|
324
|
+
private recordWakeOutcome;
|
|
325
|
+
/**
|
|
326
|
+
* Recover agents whose queued work has waited past the threshold while
|
|
327
|
+
* their session is stopped, hung or idle (CREW-303). Once per session per
|
|
328
|
+
* cooldown, with one `harness.recover` trace:
|
|
329
|
+
* - stopped → its failure backoff is cleared and one start is asked for
|
|
330
|
+
* now (through the normal start path: running-agent cap, team gates);
|
|
331
|
+
* - hung / idle → the session is stopped as a harness park, so the next
|
|
332
|
+
* pass's wake starts it fresh with its queued work in the briefing.
|
|
333
|
+
*
|
|
334
|
+
* Best-effort: never throws.
|
|
335
|
+
*
|
|
336
|
+
* @param workItems - The pass's WorkItems
|
|
337
|
+
* @param agentHealthMap - Agent health by session
|
|
338
|
+
* @param result - The pass result
|
|
339
|
+
*/
|
|
340
|
+
private recoverStalledAgents;
|
|
341
|
+
/**
|
|
342
|
+
* Recover one stalled agent (see {@link recoverStalledAgents}).
|
|
343
|
+
*
|
|
344
|
+
* @param agent - The stalled agent
|
|
345
|
+
* @param result - The pass result
|
|
346
|
+
* @param log - Logger
|
|
347
|
+
*/
|
|
348
|
+
private recoverOne;
|
|
235
349
|
}
|
|
236
350
|
//# sourceMappingURL=reconciler.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconciler.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/reconciler/reconciler.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EACV,eAAe,EAEf,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,UAAU,EACV,QAAQ,EACR,OAAO,EACP,SAAS,EACV,MAAM,yBAAyB,CAAC;AAsBjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"reconciler.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/reconciler/reconciler.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,6 +23,43 @@ import { getSettingsService } from '../settings/index.js';
|
|
|
23
23
|
import { LoggerService } from '../core/logger.service.js';
|
|
24
24
|
import { traceHarness } from '../trace/trace-recorder.js';
|
|
25
25
|
import { RestartDrainService } from '../restart/restart-drain.service.js';
|
|
26
|
+
import { detectStalledAgents } from './stalled-agent-recovery.js';
|
|
27
|
+
import { RECONCILER_WAKE_CONSTANTS, STALLED_AGENT_RECOVERY_CONSTANTS } from '../../constants.js';
|
|
28
|
+
/** Defaults from the backend constants. */
|
|
29
|
+
export const DEFAULT_WAKE_TUNING = {
|
|
30
|
+
failureBackoffBaseMs: RECONCILER_WAKE_CONSTANTS.FAILURE_BACKOFF_BASE_MS,
|
|
31
|
+
failureBackoffMaxMs: RECONCILER_WAKE_CONSTANTS.FAILURE_BACKOFF_MAX_MS,
|
|
32
|
+
startAwaitMs: RECONCILER_WAKE_CONSTANTS.START_AWAIT_MS,
|
|
33
|
+
stalledQueuedAgeMs: STALLED_AGENT_RECOVERY_CONSTANTS.QUEUED_WORK_AGE_MS,
|
|
34
|
+
stalledIdleMs: STALLED_AGENT_RECOVERY_CONSTANTS.IDLE_NO_PROGRESS_MS,
|
|
35
|
+
recoveryCooldownMs: STALLED_AGENT_RECOVERY_CONSTANTS.RECOVERY_COOLDOWN_MS,
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Normalize a provider's wake answer: `true` / `false` (older providers and
|
|
39
|
+
* tests) or a {@link WakeActionResult}.
|
|
40
|
+
*
|
|
41
|
+
* @param value - What `executeWakeAction` resolved to
|
|
42
|
+
* @returns The outcome with its reason
|
|
43
|
+
*/
|
|
44
|
+
export function normalizeWakeResult(value) {
|
|
45
|
+
if (value === true)
|
|
46
|
+
return { outcome: 'ok' };
|
|
47
|
+
if (value === false || value === undefined || value === null)
|
|
48
|
+
return { outcome: 'failed' };
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Backoff after the n-th consecutive failed wake: base doubled per failure,
|
|
53
|
+
* capped.
|
|
54
|
+
*
|
|
55
|
+
* @param failures - Consecutive failures so far (>= 1)
|
|
56
|
+
* @param tuning - Base and ceiling
|
|
57
|
+
* @returns Milliseconds before the next attempt
|
|
58
|
+
*/
|
|
59
|
+
export function wakeFailureBackoffMs(failures, tuning) {
|
|
60
|
+
const steps = Math.max(0, Math.min(failures - 1, 30));
|
|
61
|
+
return Math.min(tuning.failureBackoffBaseMs * 2 ** steps, tuning.failureBackoffMaxMs);
|
|
62
|
+
}
|
|
26
63
|
// ---------------------------------------------------------------------------
|
|
27
64
|
// ReconcilerService
|
|
28
65
|
// ---------------------------------------------------------------------------
|
|
@@ -44,6 +81,22 @@ export class ReconcilerService {
|
|
|
44
81
|
/** Where idle-holder reports go; unset = not reported (see {@link setIdleHolderReporting}). */
|
|
45
82
|
idleHolderDeps = null;
|
|
46
83
|
isRunning = false;
|
|
84
|
+
/** Wake pacing and stalled-agent thresholds */
|
|
85
|
+
wakeTuning = { ...DEFAULT_WAKE_TUNING };
|
|
86
|
+
/**
|
|
87
|
+
* Failed wakes per `session + WorkItem` (CREW-304): the next attempt waits
|
|
88
|
+
* a doubling backoff; cleared when the session wakes or the item leaves
|
|
89
|
+
* the queue.
|
|
90
|
+
*/
|
|
91
|
+
wakeFailures = new Map();
|
|
92
|
+
/** Starts still in flight (pending) per session: not requested again meanwhile */
|
|
93
|
+
pendingStarts = new Map();
|
|
94
|
+
/** Sessions whose current pending wake was already traced (one trace per episode) */
|
|
95
|
+
pendingTraced = new Set();
|
|
96
|
+
/** Last stalled-agent recovery per session (epoch ms) */
|
|
97
|
+
recoveredAt = new Map();
|
|
98
|
+
/** Sessions the last hybrid-wake pass already tried (recovery does not repeat them) */
|
|
99
|
+
wokenThisPass = new Set();
|
|
47
100
|
totalPasses = 0;
|
|
48
101
|
totalCorrections = 0;
|
|
49
102
|
/**
|
|
@@ -229,6 +282,9 @@ export class ReconcilerService {
|
|
|
229
282
|
// 8. Hybrid Wake: detect unclaimed tasks and wake dormant agents (H3)
|
|
230
283
|
// Call after status corrections to ensure we have current pool state.
|
|
231
284
|
await this.runHybridWake(workItems, agentHealthMap, result);
|
|
285
|
+
// 9. Stalled agents (CREW-303): queued work waiting on a session that is
|
|
286
|
+
// stopped, hung or idle past the threshold is recovered, once per cooldown.
|
|
287
|
+
await this.recoverStalledAgents(workItems, agentHealthMap, result);
|
|
232
288
|
});
|
|
233
289
|
}
|
|
234
290
|
/**
|
|
@@ -694,6 +750,7 @@ export class ReconcilerService {
|
|
|
694
750
|
* @param result - The ReconcileResult to accumulate wake actions into
|
|
695
751
|
*/
|
|
696
752
|
async runHybridWake(workItems, agentHealthMap, result) {
|
|
753
|
+
this.wokenThisPass = new Set();
|
|
697
754
|
// Only run if the provider supports wake actions
|
|
698
755
|
if (!this.dataProvider.executeWakeAction)
|
|
699
756
|
return;
|
|
@@ -721,39 +778,267 @@ export class ReconcilerService {
|
|
|
721
778
|
? await this.dataProvider.getAvailablePoolItems()
|
|
722
779
|
: workItems.filter(wi => wi.status === 'queued');
|
|
723
780
|
const { wakeActions } = detectUnclaimedTasks(poolItems, agentHealthMap);
|
|
724
|
-
//
|
|
781
|
+
// A WorkItem that left the queue drops its failure backoff.
|
|
782
|
+
const queuedIds = new Set(poolItems.filter((wi) => wi.status === 'queued').map((wi) => wi.id));
|
|
783
|
+
for (const [key, entry] of this.wakeFailures) {
|
|
784
|
+
if (!queuedIds.has(entry.workItemId))
|
|
785
|
+
this.wakeFailures.delete(key);
|
|
786
|
+
}
|
|
787
|
+
// One wake per session per pass, whatever the number of items (CREW-304).
|
|
788
|
+
const actedOn = new Set();
|
|
789
|
+
this.wokenThisPass = actedOn;
|
|
790
|
+
const now = Date.now();
|
|
725
791
|
for (const action of wakeActions) {
|
|
792
|
+
const session = action.agentSessionName;
|
|
793
|
+
if (actedOn.has(session))
|
|
794
|
+
continue;
|
|
726
795
|
// Belt and braces: a paused team is never woken (specs/2026-10-04-team-pause.md).
|
|
727
|
-
if (isSessionPaused(
|
|
796
|
+
if (isSessionPaused(session))
|
|
728
797
|
continue;
|
|
729
798
|
// An agent whose input box the guard keeps refusing gets a probe now and
|
|
730
799
|
// then, not a redelivery every pass (crewly#1028: ~1,100 failed
|
|
731
800
|
// redeliveries + wakes in 2h45m). Skipped attempts are counted by the
|
|
732
801
|
// breaker, not traced as failed wakes.
|
|
733
|
-
if (action.strategy === 'redeliver' && !shouldAttemptDelivery(
|
|
802
|
+
if (action.strategy === 'redeliver' && !shouldAttemptDelivery(session))
|
|
803
|
+
continue;
|
|
804
|
+
// A start still in flight (queued behind the cap) is not asked again.
|
|
805
|
+
if (this.pendingStarts.has(session))
|
|
734
806
|
continue;
|
|
807
|
+
// A failed wake waits its backoff before the next attempt.
|
|
808
|
+
const backoff = this.wakeFailures.get(this.wakeKey(session, action.workItemId));
|
|
809
|
+
if (backoff && now < backoff.nextAt)
|
|
810
|
+
continue;
|
|
811
|
+
actedOn.add(session);
|
|
735
812
|
try {
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
workItemId: action.workItemId,
|
|
739
|
-
session: action.agentSessionName,
|
|
740
|
-
summary: `Reconciler woke ${action.agentSessionName} (${action.strategy}) for queued work`,
|
|
741
|
-
outcome: success ? 'ok' : 'failed',
|
|
742
|
-
data: { strategy: action.strategy },
|
|
743
|
-
});
|
|
744
|
-
if (success) {
|
|
745
|
-
result.wakeActions.push(action);
|
|
746
|
-
result.agentsWoken++;
|
|
747
|
-
}
|
|
748
|
-
else {
|
|
749
|
-
result.errors.push(`Wake action failed for agent ${action.agentSessionName} (strategy: ${action.strategy})`);
|
|
750
|
-
}
|
|
813
|
+
const outcome = await this.attemptWake(action);
|
|
814
|
+
this.recordWakeOutcome(action, outcome, result);
|
|
751
815
|
}
|
|
752
816
|
catch (err) {
|
|
753
817
|
const message = err instanceof Error ? err.message : String(err);
|
|
754
|
-
|
|
818
|
+
this.recordWakeOutcome(action, { outcome: 'failed', reason: message }, result);
|
|
819
|
+
result.errors.push(`Wake action error for agent ${session}: ${message}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Override wake pacing and stalled-agent thresholds (tests, tuning).
|
|
825
|
+
*
|
|
826
|
+
* @param tuning - Fields to change
|
|
827
|
+
*/
|
|
828
|
+
setWakeTuning(tuning) {
|
|
829
|
+
this.wakeTuning = { ...this.wakeTuning, ...tuning };
|
|
830
|
+
}
|
|
831
|
+
/** Map key of a session + WorkItem failure backoff. */
|
|
832
|
+
wakeKey(session, workItemId) {
|
|
833
|
+
return `${session}\u0000${workItemId}`;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Run one wake. A start / rehydrate that has not finished within
|
|
837
|
+
* `startAwaitMs` (typically queued behind the running-agent cap) is
|
|
838
|
+
* reported pending and settles in the background, so the pass neither
|
|
839
|
+
* stalls nor asks again (CREW-304).
|
|
840
|
+
*
|
|
841
|
+
* @param action - The wake
|
|
842
|
+
* @returns Its outcome now (pending when still in flight)
|
|
843
|
+
*/
|
|
844
|
+
async attemptWake(action) {
|
|
845
|
+
const call = this.dataProvider.executeWakeAction(action).then(normalizeWakeResult);
|
|
846
|
+
if (action.strategy === 'redeliver')
|
|
847
|
+
return call;
|
|
848
|
+
let timer;
|
|
849
|
+
const timeout = new Promise((resolve) => {
|
|
850
|
+
timer = setTimeout(() => resolve(null), this.wakeTuning.startAwaitMs);
|
|
851
|
+
timer.unref?.();
|
|
852
|
+
});
|
|
853
|
+
const first = await Promise.race([call, timeout]);
|
|
854
|
+
if (timer)
|
|
855
|
+
clearTimeout(timer);
|
|
856
|
+
if (first)
|
|
857
|
+
return first;
|
|
858
|
+
const session = action.agentSessionName;
|
|
859
|
+
this.pendingStarts.set(session, { workItemId: action.workItemId, since: Date.now() });
|
|
860
|
+
call
|
|
861
|
+
.then((late) => {
|
|
862
|
+
this.pendingStarts.delete(session);
|
|
863
|
+
this.recordWakeOutcome(action, late, null);
|
|
864
|
+
})
|
|
865
|
+
.catch((err) => {
|
|
866
|
+
this.pendingStarts.delete(session);
|
|
867
|
+
this.recordWakeOutcome(action, { outcome: 'failed', reason: err instanceof Error ? err.message : String(err) }, null);
|
|
868
|
+
});
|
|
869
|
+
return { outcome: 'pending', reason: 'start still in progress' };
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Trace and pace one wake outcome: ok clears the session's backoffs; a
|
|
873
|
+
* failure backs that session + WorkItem off (doubling, capped); pending is
|
|
874
|
+
* traced once per episode; skipped is neither traced nor backed off.
|
|
875
|
+
*
|
|
876
|
+
* @param action - The wake
|
|
877
|
+
* @param res - Its outcome
|
|
878
|
+
* @param result - The pass result to count into (null when it settled after the pass)
|
|
879
|
+
*/
|
|
880
|
+
recordWakeOutcome(action, res, result) {
|
|
881
|
+
const session = action.agentSessionName;
|
|
882
|
+
const summary = `Reconciler woke ${session} (${action.strategy}) for queued work`;
|
|
883
|
+
if (res.outcome === 'skipped')
|
|
884
|
+
return;
|
|
885
|
+
if (res.outcome === 'pending') {
|
|
886
|
+
if (this.pendingTraced.has(session))
|
|
887
|
+
return;
|
|
888
|
+
this.pendingTraced.add(session);
|
|
889
|
+
traceHarness('harness.wake', {
|
|
890
|
+
workItemId: action.workItemId,
|
|
891
|
+
session,
|
|
892
|
+
summary: `${summary}: pending (${res.reason ?? 'start queued'})`,
|
|
893
|
+
outcome: 'queued',
|
|
894
|
+
data: { strategy: action.strategy, ...(res.reason ? { reason: res.reason } : {}) },
|
|
895
|
+
});
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
this.pendingTraced.delete(session);
|
|
899
|
+
if (res.outcome === 'ok') {
|
|
900
|
+
for (const [key, entry] of this.wakeFailures) {
|
|
901
|
+
if (entry.session === session)
|
|
902
|
+
this.wakeFailures.delete(key);
|
|
755
903
|
}
|
|
904
|
+
traceHarness('harness.wake', {
|
|
905
|
+
workItemId: action.workItemId,
|
|
906
|
+
session,
|
|
907
|
+
summary,
|
|
908
|
+
outcome: 'ok',
|
|
909
|
+
data: { strategy: action.strategy },
|
|
910
|
+
});
|
|
911
|
+
if (result) {
|
|
912
|
+
result.wakeActions.push(action);
|
|
913
|
+
result.agentsWoken++;
|
|
914
|
+
}
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const key = this.wakeKey(session, action.workItemId);
|
|
918
|
+
const failures = (this.wakeFailures.get(key)?.failures ?? 0) + 1;
|
|
919
|
+
const retryInMs = wakeFailureBackoffMs(failures, this.wakeTuning);
|
|
920
|
+
this.wakeFailures.set(key, { failures, nextAt: Date.now() + retryInMs, session, workItemId: action.workItemId });
|
|
921
|
+
traceHarness('harness.wake', {
|
|
922
|
+
workItemId: action.workItemId,
|
|
923
|
+
session,
|
|
924
|
+
summary: `${summary}: failed${res.reason ? ` (${res.reason})` : ''}; next try in ${Math.round(retryInMs / 1000)}s`,
|
|
925
|
+
outcome: 'failed',
|
|
926
|
+
data: { strategy: action.strategy, attempt: failures, retryInMs, ...(res.reason ? { reason: res.reason } : {}) },
|
|
927
|
+
});
|
|
928
|
+
if (result) {
|
|
929
|
+
result.errors.push(`Wake action failed for agent ${session} (strategy: ${action.strategy})`);
|
|
756
930
|
}
|
|
757
931
|
}
|
|
932
|
+
/**
|
|
933
|
+
* Recover agents whose queued work has waited past the threshold while
|
|
934
|
+
* their session is stopped, hung or idle (CREW-303). Once per session per
|
|
935
|
+
* cooldown, with one `harness.recover` trace:
|
|
936
|
+
* - stopped → its failure backoff is cleared and one start is asked for
|
|
937
|
+
* now (through the normal start path: running-agent cap, team gates);
|
|
938
|
+
* - hung / idle → the session is stopped as a harness park, so the next
|
|
939
|
+
* pass's wake starts it fresh with its queued work in the briefing.
|
|
940
|
+
*
|
|
941
|
+
* Best-effort: never throws.
|
|
942
|
+
*
|
|
943
|
+
* @param workItems - The pass's WorkItems
|
|
944
|
+
* @param agentHealthMap - Agent health by session
|
|
945
|
+
* @param result - The pass result
|
|
946
|
+
*/
|
|
947
|
+
async recoverStalledAgents(workItems, agentHealthMap, result) {
|
|
948
|
+
if (!this.dataProvider.executeWakeAction)
|
|
949
|
+
return;
|
|
950
|
+
const log = LoggerService.getInstance().createComponentLogger('ReconcilerService');
|
|
951
|
+
try {
|
|
952
|
+
const now = Date.now();
|
|
953
|
+
const hung = new Set(this.dataProvider.getHungAgents?.() ?? []);
|
|
954
|
+
const stalled = detectStalledAgents(workItems, agentHealthMap, {
|
|
955
|
+
now,
|
|
956
|
+
queuedAgeMs: this.wakeTuning.stalledQueuedAgeMs,
|
|
957
|
+
idleNoProgressMs: this.wakeTuning.stalledIdleMs,
|
|
958
|
+
hungSessions: hung,
|
|
959
|
+
});
|
|
960
|
+
for (const agent of stalled) {
|
|
961
|
+
const last = this.recoveredAt.get(agent.sessionName);
|
|
962
|
+
if (last !== undefined && now - last < this.wakeTuning.recoveryCooldownMs)
|
|
963
|
+
continue;
|
|
964
|
+
if (this.pendingStarts.has(agent.sessionName))
|
|
965
|
+
continue;
|
|
966
|
+
// Hybrid wake just tried to start it: that is the recovery for this pass.
|
|
967
|
+
if (agent.kind === 'stopped' && this.wokenThisPass.has(agent.sessionName))
|
|
968
|
+
continue;
|
|
969
|
+
if (agent.kind !== 'stopped' && !this.dataProvider.parkStalledAgent)
|
|
970
|
+
continue;
|
|
971
|
+
this.recoveredAt.set(agent.sessionName, now);
|
|
972
|
+
await this.recoverOne(agent, result, log);
|
|
973
|
+
}
|
|
974
|
+
// Forget sessions that are no longer stalled, so a later stall recovers at once.
|
|
975
|
+
const still = new Set(stalled.map((a) => a.sessionName));
|
|
976
|
+
for (const [session, at] of this.recoveredAt) {
|
|
977
|
+
if (!still.has(session) && now - at >= this.wakeTuning.recoveryCooldownMs)
|
|
978
|
+
this.recoveredAt.delete(session);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
catch (err) {
|
|
982
|
+
log.warn('Stalled-agent recovery pass failed (non-fatal)', {
|
|
983
|
+
error: err instanceof Error ? err.message : String(err),
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Recover one stalled agent (see {@link recoverStalledAgents}).
|
|
989
|
+
*
|
|
990
|
+
* @param agent - The stalled agent
|
|
991
|
+
* @param result - The pass result
|
|
992
|
+
* @param log - Logger
|
|
993
|
+
*/
|
|
994
|
+
async recoverOne(agent, result, log) {
|
|
995
|
+
const minutes = Math.round(agent.queuedForMs / 60_000);
|
|
996
|
+
const base = `${agent.sessionName} is ${agent.kind} with ${agent.queuedCount} queued work item(s), oldest waiting ${minutes} min`;
|
|
997
|
+
let outcome = 'failed';
|
|
998
|
+
let action = '';
|
|
999
|
+
if (agent.kind === 'stopped') {
|
|
1000
|
+
for (const [key, entry] of this.wakeFailures) {
|
|
1001
|
+
if (entry.session === agent.sessionName)
|
|
1002
|
+
this.wakeFailures.delete(key);
|
|
1003
|
+
}
|
|
1004
|
+
const wake = {
|
|
1005
|
+
workItemId: agent.workItem.id,
|
|
1006
|
+
agentSessionName: agent.sessionName,
|
|
1007
|
+
strategy: 'start',
|
|
1008
|
+
score: 0,
|
|
1009
|
+
scoreBreakdown: { skillMatch: 0, urgency: 0, contextFamiliarity: 0, loadPenalty: 0 },
|
|
1010
|
+
triggeredAt: new Date().toISOString(),
|
|
1011
|
+
...(agent.teamId ? { teamId: agent.teamId } : {}),
|
|
1012
|
+
...(agent.memberId ? { memberId: agent.memberId } : {}),
|
|
1013
|
+
};
|
|
1014
|
+
const res = await this.attemptWake(wake).catch((err) => ({
|
|
1015
|
+
outcome: 'failed',
|
|
1016
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1017
|
+
}));
|
|
1018
|
+
this.recordWakeOutcome(wake, res, result);
|
|
1019
|
+
outcome = res.outcome === 'ok' ? 'ok' : res.outcome === 'pending' ? 'queued' : res.outcome === 'skipped' ? 'queued' : 'failed';
|
|
1020
|
+
action = `start requested (${res.outcome}${res.reason ? `: ${res.reason}` : ''})`;
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
const parked = await this.dataProvider.parkStalledAgent(agent.sessionName, agent.role ?? '').catch(() => false);
|
|
1024
|
+
outcome = parked ? 'ok' : 'failed';
|
|
1025
|
+
action = parked ? 'session stopped; the next wake starts it fresh' : 'could not stop the session';
|
|
1026
|
+
}
|
|
1027
|
+
traceHarness('harness.recover', {
|
|
1028
|
+
workItemId: agent.workItem.id,
|
|
1029
|
+
session: agent.sessionName,
|
|
1030
|
+
summary: `Recovered stalled agent: ${base}; ${action}`,
|
|
1031
|
+
outcome,
|
|
1032
|
+
data: { kind: agent.kind, queuedMinutes: minutes, queuedCount: agent.queuedCount },
|
|
1033
|
+
});
|
|
1034
|
+
log.warn('Recovered a stalled agent with queued work', {
|
|
1035
|
+
sessionName: agent.sessionName,
|
|
1036
|
+
kind: agent.kind,
|
|
1037
|
+
workItemId: agent.workItem.id,
|
|
1038
|
+
queuedMinutes: minutes,
|
|
1039
|
+
queuedCount: agent.queuedCount,
|
|
1040
|
+
outcome,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
758
1043
|
}
|
|
759
1044
|
//# sourceMappingURL=reconciler.service.js.map
|