crewly 1.20.250 → 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.
Files changed (101) hide show
  1. package/config/skills/agent/core/complete-task/SKILL.md +5 -1
  2. package/config/skills/agent/core/complete-task/execute.sh +30 -15
  3. package/config/skills/agent/core/complete-task/execute.test.sh +45 -1
  4. package/dist/backend/backend/src/constants.d.ts +49 -0
  5. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  6. package/dist/backend/backend/src/constants.js +49 -0
  7. package/dist/backend/backend/src/constants.js.map +1 -1
  8. package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
  9. package/dist/backend/backend/src/controllers/team/team.controller.js +43 -4
  10. package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
  11. package/dist/backend/backend/src/index.d.ts.map +1 -1
  12. package/dist/backend/backend/src/index.js +4 -0
  13. package/dist/backend/backend/src/index.js.map +1 -1
  14. package/dist/backend/backend/src/services/agent/agent-registration.service.js +2 -2
  15. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  16. package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts +13 -0
  17. package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
  18. package/dist/backend/backend/src/services/agent/resource-mode.service.js +36 -0
  19. package/dist/backend/backend/src/services/agent/resource-mode.service.js.map +1 -1
  20. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +24 -1
  21. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
  22. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js +44 -1
  23. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
  24. package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.d.ts +48 -0
  25. package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.d.ts.map +1 -1
  26. package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.js +46 -0
  27. package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.js.map +1 -1
  28. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.d.ts +43 -1
  29. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.d.ts.map +1 -1
  30. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.js +34 -0
  31. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.js.map +1 -1
  32. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-stats.d.ts.map +1 -1
  33. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-stats.js +1 -0
  34. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-stats.js.map +1 -1
  35. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.d.ts.map +1 -1
  36. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js +23 -7
  37. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js.map +1 -1
  38. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +5 -0
  39. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
  40. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
  41. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts +18 -2
  42. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
  43. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +67 -8
  44. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
  45. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +117 -3
  46. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
  47. package/dist/backend/backend/src/services/reconciler/reconciler.service.js +304 -19
  48. package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
  49. package/dist/backend/backend/src/services/reconciler/stalled-agent-recovery.d.ts +66 -0
  50. package/dist/backend/backend/src/services/reconciler/stalled-agent-recovery.d.ts.map +1 -0
  51. package/dist/backend/backend/src/services/reconciler/stalled-agent-recovery.js +107 -0
  52. package/dist/backend/backend/src/services/reconciler/stalled-agent-recovery.js.map +1 -0
  53. package/dist/backend/backend/src/services/trace/trace-metrics.d.ts.map +1 -1
  54. package/dist/backend/backend/src/services/trace/trace-metrics.js +2 -1
  55. package/dist/backend/backend/src/services/trace/trace-metrics.js.map +1 -1
  56. package/dist/backend/backend/src/services/trace/trace.types.d.ts +1 -1
  57. package/dist/backend/backend/src/services/trace/trace.types.d.ts.map +1 -1
  58. package/dist/backend/backend/src/services/trace/trace.types.js +2 -0
  59. package/dist/backend/backend/src/services/trace/trace.types.js.map +1 -1
  60. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts +15 -0
  61. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts.map +1 -1
  62. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js +47 -0
  63. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js.map +1 -1
  64. package/dist/backend/backend/src/types/v2/index.d.ts +1 -1
  65. package/dist/backend/backend/src/types/v2/index.d.ts.map +1 -1
  66. package/dist/backend/backend/src/types/v2/index.js.map +1 -1
  67. package/dist/backend/backend/src/types/v2/reconcile.types.d.ts +20 -0
  68. package/dist/backend/backend/src/types/v2/reconcile.types.d.ts.map +1 -1
  69. package/dist/backend/backend/src/types/v2/reconcile.types.js.map +1 -1
  70. package/dist/backend/build-info.json +2 -2
  71. package/dist/cli/backend/src/constants.d.ts +49 -0
  72. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  73. package/dist/cli/backend/src/constants.js +49 -0
  74. package/dist/cli/backend/src/constants.js.map +1 -1
  75. package/dist/cli/backend/src/services/agent/agent-registration.service.js +2 -2
  76. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  77. package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts +13 -0
  78. package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
  79. package/dist/cli/backend/src/services/agent/resource-mode.service.js +36 -0
  80. package/dist/cli/backend/src/services/agent/resource-mode.service.js.map +1 -1
  81. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +24 -1
  82. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
  83. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js +44 -1
  84. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
  85. package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.d.ts +48 -0
  86. package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.d.ts.map +1 -1
  87. package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.js +46 -0
  88. package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.js.map +1 -1
  89. package/dist/cli/backend/src/services/trace/trace.types.d.ts +1 -1
  90. package/dist/cli/backend/src/services/trace/trace.types.d.ts.map +1 -1
  91. package/dist/cli/backend/src/services/trace/trace.types.js +2 -0
  92. package/dist/cli/backend/src/services/trace/trace.types.js.map +1 -1
  93. package/dist/cli/backend/src/types/v2/index.d.ts +1 -1
  94. package/dist/cli/backend/src/types/v2/index.d.ts.map +1 -1
  95. package/dist/cli/backend/src/types/v2/index.js.map +1 -1
  96. package/dist/cli/backend/src/types/v2/reconcile.types.d.ts +20 -0
  97. package/dist/cli/backend/src/types/v2/reconcile.types.d.ts.map +1 -1
  98. package/dist/cli/backend/src/types/v2/reconcile.types.js.map +1 -1
  99. package/frontend/dist/assets/{index-2db17f1e.js → index-83188f0b.js} +2 -2
  100. package/frontend/dist/index.html +1 -1
  101. 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
- /** Execute a wake action — rehydrate a suspended agent or start an inactive one */
60
- executeWakeAction?(action: WakeAction): Promise<boolean>;
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":"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"}
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
- // Execute each wake action
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(action.agentSessionName))
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(action.agentSessionName))
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 success = await this.dataProvider.executeWakeAction(action);
737
- traceHarness('harness.wake', {
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
- result.errors.push(`Wake action error for agent ${action.agentSessionName}: ${message}`);
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