@telora/daemon 0.21.134 → 0.21.144
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build-info.json +2 -2
- package/dist/audit/audit-hooks.d.ts +15 -6
- package/dist/audit/audit-hooks.d.ts.map +1 -1
- package/dist/audit/audit-hooks.js +56 -28
- package/dist/audit/audit-hooks.js.map +1 -1
- package/dist/ci/ci-escalation.js +1 -1
- package/dist/ci/ci-escalation.js.map +1 -1
- package/dist/ci/ci-sensor-loop.d.ts +24 -0
- package/dist/ci/ci-sensor-loop.d.ts.map +1 -1
- package/dist/ci/ci-sensor-loop.js +35 -9
- package/dist/ci/ci-sensor-loop.js.map +1 -1
- package/dist/cli/connect/config-write.d.ts +87 -0
- package/dist/cli/connect/config-write.d.ts.map +1 -0
- package/dist/cli/connect/config-write.js +255 -0
- package/dist/cli/connect/config-write.js.map +1 -0
- package/dist/cli/connect/registration.d.ts +76 -0
- package/dist/cli/connect/registration.d.ts.map +1 -0
- package/dist/cli/connect/registration.js +207 -0
- package/dist/cli/connect/registration.js.map +1 -0
- package/dist/cli/connect.d.ts +3 -141
- package/dist/cli/connect.d.ts.map +1 -1
- package/dist/cli/connect.js +9 -441
- package/dist/cli/connect.js.map +1 -1
- package/dist/completion/coverage-stabilized.d.ts +1 -1
- package/dist/completion/coverage-stabilized.d.ts.map +1 -1
- package/dist/completion/coverage-stabilized.js +1 -1
- package/dist/completion/coverage-stabilized.js.map +1 -1
- package/dist/completion/escalation-provenance.d.ts +2 -1
- package/dist/completion/escalation-provenance.d.ts.map +1 -1
- package/dist/completion/escalation-provenance.js +2 -1
- package/dist/completion/escalation-provenance.js.map +1 -1
- package/dist/completion/escalations.js +1 -1
- package/dist/completion/escalations.js.map +1 -1
- package/dist/completion/event-escalations.d.ts +1 -1
- package/dist/completion/event-escalations.d.ts.map +1 -1
- package/dist/completion/event-escalations.js +1 -1
- package/dist/completion/event-escalations.js.map +1 -1
- package/dist/completion/event.d.ts +1 -1
- package/dist/completion/event.d.ts.map +1 -1
- package/dist/completion/event.js +2 -2
- package/dist/completion/event.js.map +1 -1
- package/dist/completion/machine-remediable-demotion.d.ts +1 -1
- package/dist/completion/machine-remediable-demotion.d.ts.map +1 -1
- package/dist/completion/machine-remediable-demotion.js +2 -1
- package/dist/completion/machine-remediable-demotion.js.map +1 -1
- package/dist/completion/read-only-pristine.d.ts +1 -1
- package/dist/completion/read-only-pristine.d.ts.map +1 -1
- package/dist/completion/read-only-pristine.js +1 -1
- package/dist/completion/read-only-pristine.js.map +1 -1
- package/dist/completion/review-exit-escalations.d.ts +1 -1
- package/dist/completion/review-exit-escalations.d.ts.map +1 -1
- package/dist/completion/review-exit-phase.d.ts +5 -2
- package/dist/completion/review-exit-phase.d.ts.map +1 -1
- package/dist/completion/review-exit-phase.js +5 -2
- package/dist/completion/review-exit-phase.js.map +1 -1
- package/dist/completion/state-cascade.js +1 -1
- package/dist/completion/state-cascade.js.map +1 -1
- package/dist/completion/status-advance-phase.d.ts.map +1 -1
- package/dist/completion/status-advance-phase.js +2 -1
- package/dist/completion/status-advance-phase.js.map +1 -1
- package/dist/completion/team-lifecycle.d.ts.map +1 -1
- package/dist/completion/team-lifecycle.js +2 -1
- package/dist/completion/team-lifecycle.js.map +1 -1
- package/dist/delivery/delivery-advance-block.d.ts +1 -1
- package/dist/delivery/delivery-advance-block.d.ts.map +1 -1
- package/dist/delivery/delivery-advance-block.js +1 -1
- package/dist/delivery/delivery-advance-block.js.map +1 -1
- package/dist/delivery/delivery-lifecycle.js +1 -1
- package/dist/delivery/delivery-lifecycle.js.map +1 -1
- package/dist/delivery/delivery-merge-ops.js +1 -1
- package/dist/delivery/delivery-merge-ops.js.map +1 -1
- package/dist/delivery/delivery-reconciler-shadow.d.ts +1 -1
- package/dist/delivery/delivery-reconciler-shadow.d.ts.map +1 -1
- package/dist/delivery/delivery-reconciler-shadow.js +1 -1
- package/dist/delivery/delivery-reconciler-shadow.js.map +1 -1
- package/dist/delivery/operator-hold.d.ts +1 -1
- package/dist/delivery/operator-hold.d.ts.map +1 -1
- package/dist/delivery/paused-revival.d.ts.map +1 -1
- package/dist/delivery/paused-revival.js +2 -1
- package/dist/delivery/paused-revival.js.map +1 -1
- package/dist/directive/directive-dispatch.d.ts +1 -1
- package/dist/directive/directive-dispatch.d.ts.map +1 -1
- package/dist/directive/directive-dispatch.js +1 -1
- package/dist/directive/directive-dispatch.js.map +1 -1
- package/dist/directive/directive-executor.js +1 -1
- package/dist/directive/directive-executor.js.map +1 -1
- package/dist/drift/staleness-sensor.js +1 -1
- package/dist/drift/staleness-sensor.js.map +1 -1
- package/dist/focus/focus-engine.js +3 -2
- package/dist/focus/focus-engine.js.map +1 -1
- package/dist/focus/focus-lifecycle.d.ts.map +1 -1
- package/dist/focus/focus-lifecycle.js +2 -1
- package/dist/focus/focus-lifecycle.js.map +1 -1
- package/dist/focus/focus-respawn-guard.d.ts +1 -1
- package/dist/focus/focus-respawn-guard.d.ts.map +1 -1
- package/dist/focus/focus-respawn-guard.js +1 -1
- package/dist/focus/focus-respawn-guard.js.map +1 -1
- package/dist/focus/listener-auto-advance.d.ts +4 -45
- package/dist/focus/listener-auto-advance.d.ts.map +1 -1
- package/dist/focus/listener-auto-advance.js +5 -238
- package/dist/focus/listener-auto-advance.js.map +1 -1
- package/dist/focus/listener-spawn.d.ts +62 -0
- package/dist/focus/listener-spawn.d.ts.map +1 -0
- package/dist/focus/listener-spawn.js +418 -0
- package/dist/focus/listener-spawn.js.map +1 -0
- package/dist/focus/listener-state.d.ts +57 -0
- package/dist/focus/listener-state.d.ts.map +1 -0
- package/dist/focus/listener-state.js +63 -0
- package/dist/focus/listener-state.js.map +1 -0
- package/dist/focus/listener.d.ts +0 -58
- package/dist/focus/listener.d.ts.map +1 -1
- package/dist/focus/listener.js +25 -453
- package/dist/focus/listener.js.map +1 -1
- package/dist/focus/verify-open-issues.d.ts +62 -0
- package/dist/focus/verify-open-issues.d.ts.map +1 -0
- package/dist/focus/verify-open-issues.js +265 -0
- package/dist/focus/verify-open-issues.js.map +1 -0
- package/dist/focus-stage-lifecycle/index.d.ts +1 -1
- package/dist/focus-stage-lifecycle/index.d.ts.map +1 -1
- package/dist/focus-stage-lifecycle/index.js +1 -1
- package/dist/focus-stage-lifecycle/index.js.map +1 -1
- package/dist/focus-stage-lifecycle/wedge-watchdog.d.ts +1 -1
- package/dist/focus-stage-lifecycle/wedge-watchdog.d.ts.map +1 -1
- package/dist/focus-stage-lifecycle/wedge-watchdog.js +1 -1
- package/dist/focus-stage-lifecycle/wedge-watchdog.js.map +1 -1
- package/dist/git/index.d.ts +2 -0
- package/dist/git/index.d.ts.map +1 -1
- package/dist/git/index.js +2 -0
- package/dist/git/index.js.map +1 -1
- package/dist/git/merge-strategies.d.ts +80 -0
- package/dist/git/merge-strategies.d.ts.map +1 -0
- package/dist/git/merge-strategies.js +218 -0
- package/dist/git/merge-strategies.js.map +1 -0
- package/dist/git/merge-to-main.d.ts +76 -0
- package/dist/git/merge-to-main.d.ts.map +1 -0
- package/dist/git/merge-to-main.js +256 -0
- package/dist/git/merge-to-main.js.map +1 -0
- package/dist/git/merge.d.ts +27 -81
- package/dist/git/merge.d.ts.map +1 -1
- package/dist/git/merge.js +4 -448
- package/dist/git/merge.js.map +1 -1
- package/dist/git/state-detector.js +1 -1
- package/dist/git/state-detector.js.map +1 -1
- package/dist/loops/engine.js +1 -1
- package/dist/loops/engine.js.map +1 -1
- package/dist/merge/focus-merge.d.ts +1 -1
- package/dist/merge/focus-merge.d.ts.map +1 -1
- package/dist/merge/focus-merge.js +1 -1
- package/dist/merge/focus-merge.js.map +1 -1
- package/dist/merge/merge-back-loop.d.ts +1 -1
- package/dist/merge/merge-back-loop.d.ts.map +1 -1
- package/dist/merge/merge-back-loop.js +2 -2
- package/dist/merge/merge-back-loop.js.map +1 -1
- package/dist/merge/merge-detector.js +1 -1
- package/dist/merge/merge-detector.js.map +1 -1
- package/dist/nexus/return.js +1 -1
- package/dist/nexus/return.js.map +1 -1
- package/dist/prd/controller-shared.d.ts +223 -0
- package/dist/prd/controller-shared.d.ts.map +1 -0
- package/dist/prd/controller-shared.js +77 -0
- package/dist/prd/controller-shared.js.map +1 -0
- package/dist/prd/controller.d.ts +5 -256
- package/dist/prd/controller.d.ts.map +1 -1
- package/dist/prd/controller.js +9 -166
- package/dist/prd/controller.js.map +1 -1
- package/dist/prd/done-sweep.d.ts +55 -0
- package/dist/prd/done-sweep.d.ts.map +1 -0
- package/dist/prd/done-sweep.js +112 -0
- package/dist/prd/done-sweep.js.map +1 -0
- package/dist/prompt-sections/execution-sections.d.ts +1 -1
- package/dist/prompt-sections/execution-sections.d.ts.map +1 -1
- package/dist/prompt-sections/execution-sections.js +12 -2
- package/dist/prompt-sections/execution-sections.js.map +1 -1
- package/dist/prompt-sections/shared.d.ts +1 -1
- package/dist/prompt-sections/shared.d.ts.map +1 -1
- package/dist/queries/agent-resolvable-promotion.d.ts +1 -1
- package/dist/queries/agent-resolvable-promotion.d.ts.map +1 -1
- package/dist/queries/agent-resolvable-promotion.js +2 -1
- package/dist/queries/agent-resolvable-promotion.js.map +1 -1
- package/dist/queries/deliveries.d.ts +0 -195
- package/dist/queries/deliveries.d.ts.map +1 -1
- package/dist/queries/deliveries.js +1 -83
- package/dist/queries/deliveries.js.map +1 -1
- package/dist/queries/escalation-consumption.d.ts +120 -0
- package/dist/queries/escalation-consumption.d.ts.map +1 -0
- package/dist/queries/escalation-consumption.js +143 -0
- package/dist/queries/escalation-consumption.js.map +1 -0
- package/dist/queries/escalation-dispositions.d.ts +19 -0
- package/dist/queries/escalation-dispositions.d.ts.map +1 -0
- package/dist/queries/escalation-dispositions.js +18 -0
- package/dist/queries/escalation-dispositions.js.map +1 -0
- package/dist/queries/escalation-suppression.d.ts +1 -1
- package/dist/queries/escalation-suppression.d.ts.map +1 -1
- package/dist/queries/escalation-suppression.js +1 -1
- package/dist/queries/escalation-suppression.js.map +1 -1
- package/dist/queries/escalations.d.ts +343 -0
- package/dist/queries/escalations.d.ts.map +1 -0
- package/dist/queries/escalations.js +493 -0
- package/dist/queries/escalations.js.map +1 -0
- package/dist/queries/focus-discovery.d.ts +204 -0
- package/dist/queries/focus-discovery.d.ts.map +1 -0
- package/dist/queries/focus-discovery.js +92 -0
- package/dist/queries/focus-discovery.js.map +1 -0
- package/dist/queries/index.d.ts +4 -2
- package/dist/queries/index.d.ts.map +1 -1
- package/dist/queries/index.js +4 -2
- package/dist/queries/index.js.map +1 -1
- package/dist/queries/issues.d.ts +8 -436
- package/dist/queries/issues.d.ts.map +1 -1
- package/dist/queries/issues.js +7 -615
- package/dist/queries/issues.js.map +1 -1
- package/dist/resolvers/agent-escalations.d.ts +20 -0
- package/dist/resolvers/agent-escalations.d.ts.map +1 -1
- package/dist/resolvers/agent-escalations.js +40 -10
- package/dist/resolvers/agent-escalations.js.map +1 -1
- package/dist/resolvers/index.d.ts +2 -2
- package/dist/resolvers/index.d.ts.map +1 -1
- package/dist/resolvers/index.js +1 -1
- package/dist/resolvers/index.js.map +1 -1
- package/dist/resolvers/shared/git-diff.d.ts +99 -18
- package/dist/resolvers/shared/git-diff.d.ts.map +1 -1
- package/dist/resolvers/shared/git-diff.js +98 -27
- package/dist/resolvers/shared/git-diff.js.map +1 -1
- package/dist/shell/control-state.d.ts +1 -1
- package/dist/shell/control-state.d.ts.map +1 -1
- package/dist/shell/control-state.js +1 -1
- package/dist/shell/control-state.js.map +1 -1
- package/dist/shell/crash-recovery.d.ts +1 -1
- package/dist/shell/crash-recovery.d.ts.map +1 -1
- package/dist/shell/crash-recovery.js +2 -2
- package/dist/shell/crash-recovery.js.map +1 -1
- package/dist/shell/install-probe-tick.d.ts +1 -1
- package/dist/shell/install-probe-tick.d.ts.map +1 -1
- package/dist/shell/install-probe-tick.js +1 -1
- package/dist/shell/install-probe-tick.js.map +1 -1
- package/dist/shell/unified-shell.js +1 -1
- package/dist/shell/unified-shell.js.map +1 -1
- package/dist/spawn/model-routing.d.ts +1 -1
- package/dist/spawn/model-routing.d.ts.map +1 -1
- package/dist/spawn/model-routing.js +1 -1
- package/dist/spawn/model-routing.js.map +1 -1
- package/dist/spawn/prompt-assembly.d.ts +1 -1
- package/dist/spawn/prompt-assembly.d.ts.map +1 -1
- package/dist/spawn/prompt-assembly.js +1 -1
- package/dist/spawn/prompt-assembly.js.map +1 -1
- package/dist/spawn/spawn-decision.js +1 -1
- package/dist/spawn/spawn-decision.js.map +1 -1
- package/dist/spawn/worktree-session.d.ts +1 -1
- package/dist/spawn/worktree-session.d.ts.map +1 -1
- package/dist/spawn/worktree-session.js +1 -1
- package/dist/spawn/worktree-session.js.map +1 -1
- package/dist/templates/claude-md.d.ts +1 -1
- package/dist/templates/claude-md.d.ts.map +1 -1
- package/dist/templates/claude-md.js +12 -0
- package/dist/templates/claude-md.js.map +1 -1
- package/dist/verify/verification-default-on.d.ts +1 -1
- package/dist/verify/verification-default-on.d.ts.map +1 -1
- package/dist/verify/verification-deps.d.ts +1 -1
- package/dist/verify/verification-deps.d.ts.map +1 -1
- package/dist/verify/verification-engine.d.ts.map +1 -1
- package/dist/verify/verification-engine.js +20 -12
- package/dist/verify/verification-engine.js.map +1 -1
- package/dist/worktree/provision.js +1 -1
- package/dist/worktree/provision.js.map +1 -1
- package/dist/worktree/scratch-guard.d.ts.map +1 -1
- package/dist/worktree/scratch-guard.js +23 -2
- package/dist/worktree/scratch-guard.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../../src/focus/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../../src/focus/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAiBtD,OAAO,EAAuB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvE,OAAO,EAAE,YAAY,EAAE,CAAC;AAuCxB;;GAEG;AACH,wBAAsB,YAAY,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAyE5E;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAsH5C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAO3C;AAGD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAmIxD;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAMnD;AAED;;GAEG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAUlD"}
|
package/dist/focus/listener.js
CHANGED
|
@@ -7,15 +7,14 @@
|
|
|
7
7
|
* single Agent Team per focus that manages all deliveries.
|
|
8
8
|
*/
|
|
9
9
|
import { getAgentRoles } from '../queries/roles.js';
|
|
10
|
-
import { checkDeliveryAutoAdvance,
|
|
10
|
+
import { checkDeliveryAutoAdvance, checkDeliveryWorkStart } from './listener-auto-advance.js';
|
|
11
|
+
import { checkVerifyDeliveriesForOpenIssues } from './verify-open-issues.js';
|
|
11
12
|
import { checkPausedDeliveriesForRevival, buildDefaultPausedRevivalDeps } from '../delivery/paused-revival.js';
|
|
12
13
|
import { runReconcilerShadow } from '../delivery/delivery-reconciler-shadow.js';
|
|
13
|
-
import {
|
|
14
|
-
import { applyRespawnGuard } from './focus-respawn-guard.js';
|
|
14
|
+
import { getActiveTeams, checkAndMergeCompletedDeliveries } from './focus-executor.js';
|
|
15
15
|
import { checkResolvedMergeConflicts } from '../merge/focus-merge.js';
|
|
16
16
|
import { runCascadeChecks } from '../completion/state-cascade.js';
|
|
17
17
|
import { runStageLifecycleTransitions } from '../focus-stage-lifecycle/index.js';
|
|
18
|
-
import { recordActivity } from '../shell/heartbeat.js';
|
|
19
18
|
import { safeInterval, withRetry } from '@telora/daemon-core';
|
|
20
19
|
import { getActiveSessions, updateSession } from '../queries/sessions.js';
|
|
21
20
|
import { ensureFocusWorktrees, checkForDeactivatedFocuses } from './focus-lifecycle.js';
|
|
@@ -26,77 +25,40 @@ import { isDaemonShuttingDown } from '../shell/shutdown-state.js';
|
|
|
26
25
|
import { registerPollTrigger, triggerCheck } from '../poll-trigger.js';
|
|
27
26
|
// Re-exported for existing importers; the live trigger is registered below.
|
|
28
27
|
export { triggerCheck };
|
|
29
|
-
import { configForProduct
|
|
28
|
+
import { configForProduct } from '../shell/config.js';
|
|
30
29
|
import { checkIncomingRequests } from '../nexus/index.js';
|
|
31
30
|
import { checkIntakePendingRequests } from '../nexus/index.js';
|
|
32
31
|
import { checkResolvedRequests } from '../nexus/index.js';
|
|
33
32
|
import { checkEndpointClearance } from '../nexus/index.js';
|
|
34
|
-
import { checkDirectives, seedLastProcessedStage
|
|
35
|
-
import {
|
|
36
|
-
import { getActiveFocuses, getReadyFocuses, clearAnswerRearm } from '../queries/deliveries.js';
|
|
37
|
-
import { isAuditPhaseFocusRow } from '../audit/audit-shape.js';
|
|
38
|
-
import { isIntakeDraining } from '../shell/intake-control.js';
|
|
33
|
+
import { checkDirectives, seedLastProcessedStage } from '../directive/directive-executor.js';
|
|
34
|
+
import { getActiveFocuses } from '../queries/focus-discovery.js';
|
|
39
35
|
import { checkDiscoveryRequests } from '../host-control/discovery-poll.js';
|
|
40
|
-
// Track focuses that have been logged as idle (no actionable deliveries).
|
|
41
|
-
// Only log on state change: first skip after an active session or after startup.
|
|
42
|
-
const idleLoggedFocuses = new Set();
|
|
43
|
-
// Track focuses already warned this blocked episode for a null-role coding
|
|
44
|
-
// off-state pending directive (Play/queue toggle cleared, no operator stop).
|
|
45
|
-
// Without this, the "no assigned_agent_role_id; skipping spawn (coding off-state)"
|
|
46
|
-
// warning fires every 30s poll for as long as the directive sits queued. We warn
|
|
47
|
-
// once on entry and suppress until the focus resumes (role assigned, directive
|
|
48
|
-
// cleared, or focus leaves the active set), at which point it is removed so a
|
|
49
|
-
// later re-entry warns again. Operator-stop suppression does NOT use this set --
|
|
50
|
-
// it drops the directive, so it is inherently once-per-episode.
|
|
51
|
-
const nullRoleSkipWarnedFocuses = new Set();
|
|
52
|
-
// Depth of the ready-focus queue as of the most recent completed poll. Read by
|
|
53
|
-
// the auto-updater's batch-safe idle predicate (auto-update-idle.ts): a drained
|
|
54
|
-
// queue (0) plus no active team is a safe restart seam. Stale-but-recent is fine
|
|
55
|
-
// -- the idle gate is debounced well past the poll interval.
|
|
56
|
-
let lastReadyFocusCount = 0;
|
|
57
|
-
/**
|
|
58
|
-
* Number of focuses with actionable (queued) work as of the last poll. Zero
|
|
59
|
-
* means the focus queue is drained. Used by the batch-safe idle predicate so a
|
|
60
|
-
* transient between-delivery gap (which still has queued focuses) does not read
|
|
61
|
-
* as idle.
|
|
62
|
-
*/
|
|
63
|
-
export function getReadyFocusQueueDepth() {
|
|
64
|
-
return lastReadyFocusCount;
|
|
65
|
-
}
|
|
66
|
-
let config = null;
|
|
67
|
-
const roles = new Map();
|
|
68
|
-
let pollInterval = null;
|
|
69
|
-
let pollState = 'idle';
|
|
70
|
-
// Polling interval in milliseconds (default: 30 seconds)
|
|
71
|
-
const POLL_INTERVAL_MS = 30000;
|
|
72
|
-
// Roles refresh: re-fetch from DB periodically to pick up changes
|
|
73
|
-
const ROLES_REFRESH_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
74
|
-
let lastRolesRefreshAt = 0;
|
|
75
|
-
// Health check backoff: track consecutive failures to avoid hammering a broken API
|
|
76
|
-
let healthCheckConsecutiveFailures = 0;
|
|
77
|
-
const HEALTH_CHECK_MAX_SKIP_CYCLES = 10; // max backoff = 10 * 30s = 5 minutes
|
|
78
36
|
// Spawn cooldown imported from dedicated module (avoids circular deps)
|
|
79
|
-
import {
|
|
37
|
+
import { hydrateSpawnCooldownEntry } from '../spawn/spawn-cooldown.js';
|
|
80
38
|
import { hydrateDeliveryAdvancementState } from '../completion/delivery-advancement-guard.js';
|
|
81
39
|
import { hydrateBelowCoverageDriftCount } from '../completion/coverage-drift-state.js';
|
|
82
|
-
import { shouldSkipPendingSpawn, recordPendingSpawnAttempt } from '../spawn/index.js';
|
|
83
|
-
import { fileRespawnCycleExhaustedEscalation } from '../spawn/respawn-cycle-escalation.js';
|
|
84
40
|
import { createLogger } from '../shell/log.js';
|
|
85
41
|
import { runWithTickDeadline, DEFAULT_TICK_DEADLINE_MS } from './tick-deadline.js';
|
|
42
|
+
// Poll-loop singletons live in listener-state.ts (the leaf); the spawn-processing
|
|
43
|
+
// pass in listener-spawn.ts. Reads use the live bindings; reassignments go
|
|
44
|
+
// through the setters. Both this module and listener-spawn.ts import the state
|
|
45
|
+
// leaf -- no cycle.
|
|
46
|
+
import { config, roles, pollInterval, healthCheckConsecutiveFailures, POLL_INTERVAL_MS, HEALTH_CHECK_MAX_SKIP_CYCLES, setConfig, setPollInterval, setPollState, setHealthCheckConsecutiveFailures, setLastRolesRefreshAt, } from './listener-state.js';
|
|
47
|
+
import { processReadyFocuses, processPendingSpawnFocuses } from './listener-spawn.js';
|
|
86
48
|
const log = createLogger({ module: 'listener' });
|
|
87
49
|
/**
|
|
88
50
|
* Initialize the listener with configuration.
|
|
89
51
|
*/
|
|
90
52
|
export async function initListener(daemonConfig) {
|
|
91
|
-
|
|
53
|
+
setConfig(daemonConfig);
|
|
92
54
|
// Load all roles
|
|
93
55
|
try {
|
|
94
|
-
const roleList = await getAgentRoles(
|
|
56
|
+
const roleList = await getAgentRoles(daemonConfig.organizationId);
|
|
95
57
|
roles.clear();
|
|
96
58
|
for (const role of roleList) {
|
|
97
59
|
roles.set(role.id, role);
|
|
98
60
|
}
|
|
99
|
-
|
|
61
|
+
setLastRolesRefreshAt(Date.now());
|
|
100
62
|
log.info(`Loaded ${roles.size} agent roles`);
|
|
101
63
|
// Seed directive executor with current stage IDs to prevent
|
|
102
64
|
// re-firing directives for stages already processed before restart.
|
|
@@ -172,7 +134,7 @@ export function startDeliveryListener() {
|
|
|
172
134
|
// Start polling with error boundary — check for work and main advancement
|
|
173
135
|
// Per-product functions are called with product-scoped configs so that
|
|
174
136
|
// config.productId and config.repoPath resolve to the correct product.
|
|
175
|
-
|
|
137
|
+
setPollInterval(safeInterval(async () => {
|
|
176
138
|
// Deadline watchdog (TEL-3): surface a poll tick that stops making progress.
|
|
177
139
|
// safeInterval has no deadline and recordActivity() (heartbeat) is emitted
|
|
178
140
|
// inside this body, so a hung tick is silent AND stops the heartbeat -- from
|
|
@@ -269,7 +231,7 @@ export function startDeliveryListener() {
|
|
|
269
231
|
// review phase it already fetched) rather than fanning out a second pass.
|
|
270
232
|
await checkSessionHealth();
|
|
271
233
|
});
|
|
272
|
-
}, POLL_INTERVAL_MS, 'focus-poll', { skipIfRunning: true });
|
|
234
|
+
}, POLL_INTERVAL_MS, 'focus-poll', { skipIfRunning: true }));
|
|
273
235
|
log.info(`Started focus polling (every ${POLL_INTERVAL_MS / 1000}s)`);
|
|
274
236
|
}
|
|
275
237
|
/**
|
|
@@ -283,396 +245,6 @@ export function startSessionListener() {
|
|
|
283
245
|
// In polling mode, session changes are handled by checking ready focuses
|
|
284
246
|
log.info('Session monitoring via polling (included in focus polling)');
|
|
285
247
|
}
|
|
286
|
-
export function decideFocusSpawn(gate) {
|
|
287
|
-
if (!gate.hasRole)
|
|
288
|
-
return 'skip_unknown_role';
|
|
289
|
-
if (gate.hasActiveTeam)
|
|
290
|
-
return 'skip_active_team';
|
|
291
|
-
if (gate.inSpawnCooldown)
|
|
292
|
-
return 'skip_cooldown';
|
|
293
|
-
if (gate.activeTeamCount >= gate.maxTotalSessions)
|
|
294
|
-
return 'stop_capacity';
|
|
295
|
-
return 'spawn';
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Process any focuses ready for agent work.
|
|
299
|
-
*
|
|
300
|
-
* For each ready focus without an active team, spawns a single
|
|
301
|
-
* Agent Team that manages all deliveries within that focus.
|
|
302
|
-
* Also detects focuses that have been deactivated and shuts down
|
|
303
|
-
* their teams.
|
|
304
|
-
*/
|
|
305
|
-
export async function processReadyFocuses() {
|
|
306
|
-
if (!config)
|
|
307
|
-
return;
|
|
308
|
-
// Prevent concurrent processing, and skip cycles during health check backoff
|
|
309
|
-
if (pollState === 'processing' || pollState === 'backing_off') {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
pollState = 'processing';
|
|
313
|
-
try {
|
|
314
|
-
// Periodically refresh agent roles to pick up changes
|
|
315
|
-
if (Date.now() - lastRolesRefreshAt > ROLES_REFRESH_INTERVAL_MS) {
|
|
316
|
-
try {
|
|
317
|
-
const roleList = await getAgentRoles(config.organizationId);
|
|
318
|
-
roles.clear();
|
|
319
|
-
for (const role of roleList) {
|
|
320
|
-
roles.set(role.id, role);
|
|
321
|
-
}
|
|
322
|
-
lastRolesRefreshAt = Date.now();
|
|
323
|
-
log.info(`Refreshed agent roles (${roles.size} roles loaded)`);
|
|
324
|
-
}
|
|
325
|
-
catch (err) {
|
|
326
|
-
log.warn(`Failed to refresh agent roles: ${err.message}`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
// Record activity: proves the poll ran even if no work was found
|
|
330
|
-
recordActivity();
|
|
331
|
-
// Snapshot active teams before processing (for idle detection)
|
|
332
|
-
const previouslyActiveIds = new Set(getActiveTeams().keys());
|
|
333
|
-
// Deactivation detection is now handled by checkForDeactivatedFocuses()
|
|
334
|
-
// in the poll loop (called before processReadyFocuses).
|
|
335
|
-
// Get ready focuses across all configured products
|
|
336
|
-
const readyFocuses = [];
|
|
337
|
-
for (const product of config.products) {
|
|
338
|
-
try {
|
|
339
|
-
const focuses = await getReadyFocuses(config.organizationId, product.id);
|
|
340
|
-
readyFocuses.push(...focuses);
|
|
341
|
-
}
|
|
342
|
-
catch (err) {
|
|
343
|
-
log.warn(`Failed to fetch ready focuses for product ${product.id}: ${err.message}`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
// Publish the queue depth for the auto-updater's batch-safe idle predicate.
|
|
347
|
-
// Set before any early-return below so a drained queue is observable.
|
|
348
|
-
lastReadyFocusCount = readyFocuses.length;
|
|
349
|
-
// Track which focuses are ready this cycle (for idle logging)
|
|
350
|
-
const readyFocusIds = new Set(readyFocuses.map(s => s.focus_id));
|
|
351
|
-
// Idle queue logging: detect focuses that were previously active
|
|
352
|
-
// but now have no ready deliveries. Only log on state change.
|
|
353
|
-
for (const focusId of previouslyActiveIds) {
|
|
354
|
-
if (!readyFocusIds.has(focusId) && !hasActiveTeam(focusId) && !idleLoggedFocuses.has(focusId)) {
|
|
355
|
-
const nextPollSec = POLL_INTERVAL_MS / 1000;
|
|
356
|
-
log.info(`Focus queue idle: "${focusId}" -- no actionable deliveries, next poll in ${nextPollSec}s`);
|
|
357
|
-
idleLoggedFocuses.add(focusId);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
if (readyFocuses.length === 0) {
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
// Intake drain (auto-update delivery C): a pending update has been stale
|
|
364
|
-
// past threshold while the daemon stays busy, so we stop accepting new focus
|
|
365
|
-
// teams to let the current batch become the safe seam. Active teams continue
|
|
366
|
-
// to completion; we simply do not spawn more. The held queue does not block
|
|
367
|
-
// the batch boundary because isBatchSafeIdle honors isIntakeDraining().
|
|
368
|
-
if (isIntakeDraining()) {
|
|
369
|
-
log.info(`[listener] Intake drained for pending update -- holding ${readyFocuses.length} ready focus(es); no new teams will spawn`);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
const productLabel = config.products.length > 1
|
|
373
|
-
? ` across ${config.products.length} product(s)`
|
|
374
|
-
: '';
|
|
375
|
-
log.info(`Found ${readyFocuses.length} ready focuses${productLabel}`);
|
|
376
|
-
for (const focus of readyFocuses) {
|
|
377
|
-
const role = roles.get(focus.assigned_agent_role_id);
|
|
378
|
-
// Resolve the per-focus spawn-gate decision (pure; see decideFocusSpawn).
|
|
379
|
-
// The capacity input is the LIVE team count sampled this iteration, so a
|
|
380
|
-
// spawn earlier in this loop is reflected for the next focus.
|
|
381
|
-
const action = decideFocusSpawn({
|
|
382
|
-
hasRole: role !== undefined,
|
|
383
|
-
hasActiveTeam: hasActiveTeam(focus.focus_id),
|
|
384
|
-
inSpawnCooldown: isInSpawnCooldown(focus.focus_id),
|
|
385
|
-
activeTeamCount: getActiveTeamCount(),
|
|
386
|
-
maxTotalSessions: config.maxTotalSessions,
|
|
387
|
-
});
|
|
388
|
-
if (action === 'skip_unknown_role') {
|
|
389
|
-
log.error(`Unknown agent role: ${focus.assigned_agent_role_id}`);
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
if (action === 'skip_active_team') {
|
|
393
|
-
// Focus is active -- clear idle state so we re-log if it goes idle again
|
|
394
|
-
idleLoggedFocuses.delete(focus.focus_id);
|
|
395
|
-
// A re-arm flag may have landed while a team is up. Do NOT clear it here:
|
|
396
|
-
// the answer is consumed only at (re)spawn time via the escalation.answers
|
|
397
|
-
// resolver, and a still-live team does not re-run assembly mid-session
|
|
398
|
-
// (the wake-message handoff does not carry answers). Leaving the flag set
|
|
399
|
-
// lets the force-spawn deliver the answer once this team exits; clearing
|
|
400
|
-
// it now would strand the answer. The clear happens on the force-spawn
|
|
401
|
-
// below (one answer -> one re-spawn).
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
// Skip focuses that were recently torn down (spawn cooldown).
|
|
405
|
-
if (action === 'skip_cooldown') {
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
// Total capacity reached -- stop the whole loop, not just this focus.
|
|
409
|
-
if (action === 'stop_capacity') {
|
|
410
|
-
log.info(`Total session limit reached, stopping`);
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
// action === 'spawn'. role is guaranteed defined (hasRole was true);
|
|
414
|
-
// this guard narrows the type and is otherwise unreachable.
|
|
415
|
-
if (!role)
|
|
416
|
-
continue;
|
|
417
|
-
const isRearm = focus.answer_rearm_requested_at != null;
|
|
418
|
-
// Clear idle state before spawn attempt -- if spawn succeeds,
|
|
419
|
-
// the focus is active. If it skips (no actionable deliveries),
|
|
420
|
-
// spawnFocusTeam logs the skip and we mark it idle here.
|
|
421
|
-
const wasIdle = idleLoggedFocuses.has(focus.focus_id);
|
|
422
|
-
idleLoggedFocuses.delete(focus.focus_id);
|
|
423
|
-
// Use product-scoped config so the team uses the correct repoPath
|
|
424
|
-
const productEntry = focus.product_id ? findProduct(config, focus.product_id) : undefined;
|
|
425
|
-
const spawnConfig = productEntry ? configForProduct(config, productEntry) : config;
|
|
426
|
-
// Spawn a focus team (one team per focus). forceSpawn lets a re-armed
|
|
427
|
-
// focus spawn past findActionableDeliveries' 'skip' to consume the
|
|
428
|
-
// pending escalation answer.
|
|
429
|
-
// Audit-phase focus (explicit read_only=true OR the second structural
|
|
430
|
-
// signal: audit_origin && not-yet-ratified, TEL-3) spawns read-only, so a
|
|
431
|
-
// flag-absent audit focus is not driven into scoping/write/commit ahead of
|
|
432
|
-
// ratification (2f253da6). See isAuditPhaseFocusRow.
|
|
433
|
-
await spawnFocusTeam({ config: spawnConfig, focusId: focus.focus_id, focusName: focus.focus_name, role, pipelineConfig: focus.pipeline_config, readOnly: isAuditPhaseFocusRow(focus), lastClaudeSessionId: focus.last_claude_session_id, claudeSessionIds: focus.claude_session_ids, engineOverride: focus.engine_override, localModelOverride: focus.local_model_override, orgDefaultEngine: focus.org_default_engine, pinnedBranch: focus.branch_name, forceSpawn: isRearm });
|
|
434
|
-
// Clear the re-arm flag only once a team is actually live to consume the
|
|
435
|
-
// answer: one answer -> one re-spawn. If the force-spawn did NOT produce an
|
|
436
|
-
// active team (worktree/governor failure, etc.), leave the flag set so a
|
|
437
|
-
// later poll retries delivery rather than stranding the answer. (Cooldown
|
|
438
|
-
// and capacity exit above without reaching here, so they also keep the
|
|
439
|
-
// flag.) Done before the idle check below so a consumed re-arm is not
|
|
440
|
-
// re-surfaced next poll.
|
|
441
|
-
if (isRearm && hasActiveTeam(focus.focus_id)) {
|
|
442
|
-
await clearAnswerRearm(focus.focus_id).catch((err) => log.warn(`Failed to clear answer re-arm for "${focus.focus_name}": ${err.message}`));
|
|
443
|
-
}
|
|
444
|
-
// If the spawn was skipped (no active team after spawn), mark as idle
|
|
445
|
-
if (!hasActiveTeam(focus.focus_id) && !wasIdle) {
|
|
446
|
-
const nextPollSec = POLL_INTERVAL_MS / 1000;
|
|
447
|
-
log.info(`Focus queue idle: "${focus.focus_name}" -- no actionable deliveries after pre-spawn check, next poll in ${nextPollSec}s`);
|
|
448
|
-
idleLoggedFocuses.add(focus.focus_id);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
catch (error) {
|
|
453
|
-
log.error('Error processing ready focuses:', error);
|
|
454
|
-
}
|
|
455
|
-
finally {
|
|
456
|
-
pollState = healthCheckConsecutiveFailures > 0 ? 'backing_off' : 'idle';
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Spawn teams for focuses that have a pending spawn directive but were
|
|
461
|
-
* not surfaced by getReadyFocuses.
|
|
462
|
-
*
|
|
463
|
-
* getReadyFocuses (daemon-focus-handlers.ts) only returns focuses with
|
|
464
|
-
* queued/coding deliveries (or zero deliveries). A review-stage spawn
|
|
465
|
-
* directive on a focus whose deliveries are all in verify never makes
|
|
466
|
-
* it to processReadyFocuses -- the pending directive sits forever and
|
|
467
|
-
* the team never starts. This function bridges that gap: scan the
|
|
468
|
-
* pending-spawn map directly and spawn for any focus that's not
|
|
469
|
-
* already running.
|
|
470
|
-
*/
|
|
471
|
-
export async function processPendingSpawnFocuses() {
|
|
472
|
-
if (!config)
|
|
473
|
-
return;
|
|
474
|
-
if (isDaemonShuttingDown())
|
|
475
|
-
return;
|
|
476
|
-
const pendingIds = getPendingSpawnFocusIds().filter(id => !hasActiveTeam(id));
|
|
477
|
-
if (pendingIds.length === 0)
|
|
478
|
-
return;
|
|
479
|
-
// Fetch focus details for all configured products. getActiveFocuses
|
|
480
|
-
// returns the metadata spawnFocusTeam needs.
|
|
481
|
-
const focusDetailsById = new Map();
|
|
482
|
-
// Whether ANY product's getActiveFocuses read failed this tick. A fetch failure
|
|
483
|
-
// makes the active set incomplete, so a pending focus can look absent purely
|
|
484
|
-
// because its product's read failed -- absence is only trustworthy when this is
|
|
485
|
-
// false. Gates the zombie-directive reap below.
|
|
486
|
-
let anyFetchFailed = false;
|
|
487
|
-
for (const product of config.products) {
|
|
488
|
-
try {
|
|
489
|
-
const focuses = await getActiveFocuses(config.organizationId, product.id);
|
|
490
|
-
for (const f of focuses) {
|
|
491
|
-
if (pendingIds.includes(f.focus_id)) {
|
|
492
|
-
focusDetailsById.set(f.focus_id, f);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
catch (err) {
|
|
497
|
-
anyFetchFailed = true;
|
|
498
|
-
log.warn(`[listener] processPendingSpawnFocuses: failed to fetch active focuses for product ${product.id}: ${err.message}`);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
for (const focusId of pendingIds) {
|
|
502
|
-
const focus = focusDetailsById.get(focusId);
|
|
503
|
-
if (!focus) {
|
|
504
|
-
// Focus is absent from the active set. If the read was complete this tick
|
|
505
|
-
// (no fetch failure), the focus has genuinely left the active set -- its
|
|
506
|
-
// directive is a zombie: reap it once instead of retrying forever. On a
|
|
507
|
-
// fetch-failure tick the absence is untrustworthy, so keep retrying.
|
|
508
|
-
if (shouldReapPendingSpawnDirective({ anyFetchFailed, focusPresentInActiveSet: false })) {
|
|
509
|
-
clearPendingSpawnDirective(focusId);
|
|
510
|
-
// Focus left the active set -- the blocked episode is over. Clear its
|
|
511
|
-
// once-per-episode warn marker so a future re-entry warns again.
|
|
512
|
-
nullRoleSkipWarnedFocuses.delete(focusId);
|
|
513
|
-
log.info(`[listener] Dropping stale spawn directive for focus ${focusId.slice(0, 8)}: ` +
|
|
514
|
-
`confirmed absent from every product's active set (focus completed/deactivated).`);
|
|
515
|
-
}
|
|
516
|
-
else {
|
|
517
|
-
log.warn(`[listener] Pending spawn directive for focus ${focusId.slice(0, 8)} but focus not found in active set; ` +
|
|
518
|
-
`active-set read incomplete this tick (fetch failure) -- retrying next poll.`);
|
|
519
|
-
}
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
let role;
|
|
523
|
-
if (!focus.assigned_agent_role_id) {
|
|
524
|
-
// A null assigned_agent_role_id now means ONE OF TWO things, and they are
|
|
525
|
-
// NOT interchangeable:
|
|
526
|
-
// - operator_stopped_at set => an operator pulled the Play toggle to
|
|
527
|
-
// halt the focus. Authoritative: it wins over the review fallback.
|
|
528
|
-
// - operator_stopped_at null => auto_terminate_focus_team() cleared the
|
|
529
|
-
// role at completion (lifecycle). Here the review fallback applies:
|
|
530
|
-
// a review is a SECOND team that must spawn AFTER completion; a coding
|
|
531
|
-
// directive is the intended off-state and must NOT auto-spawn.
|
|
532
|
-
// decideNullRoleSpawn disambiguates via operatorStopped; the listener then
|
|
533
|
-
// acts on the decision (drop the directive on a stop, throttle the
|
|
534
|
-
// off-state warning, or spawn the lifecycle review fallback).
|
|
535
|
-
const operatorStopped = !!focus.operator_stopped_at;
|
|
536
|
-
const pending = getPendingSpawnDirective(focus.focus_id);
|
|
537
|
-
const decision = decideNullRoleSpawn({
|
|
538
|
-
sessionType: pending?.sessionType,
|
|
539
|
-
fallbackRole: resolveReviewerFallbackRole(roles.values()),
|
|
540
|
-
operatorStopped,
|
|
541
|
-
});
|
|
542
|
-
if (decision.action === 'skip') {
|
|
543
|
-
if (operatorStopped) {
|
|
544
|
-
// Operator STOP wins over the review fallback. Drop any directive
|
|
545
|
-
// queued while stopped so it does not silently survive to resume and
|
|
546
|
-
// fire the path the stop was meant to prevent. Dropping also removes
|
|
547
|
-
// the focus from the pending set, so this suppression is logged once
|
|
548
|
-
// per focus (per stop), never once per poll.
|
|
549
|
-
if (pending)
|
|
550
|
-
clearPendingSpawnDirective(focus.focus_id);
|
|
551
|
-
nullRoleSkipWarnedFocuses.delete(focus.focus_id);
|
|
552
|
-
log.info(`[listener] Focus ${focusId.slice(0, 8)} ("${focus.focus_name}") is held by an operator stop; ` +
|
|
553
|
-
`suppressing the queued ${pending?.sessionType ?? 'spawn'} directive (dropped, not carried to resume).`);
|
|
554
|
-
continue;
|
|
555
|
-
}
|
|
556
|
-
// Coding off-state (Play/queue toggle cleared, no operator stop): the
|
|
557
|
-
// directive legitimately stays queued. Bound the warning to once per
|
|
558
|
-
// blocked episode so a stopped focus does not spin-log every 30s poll.
|
|
559
|
-
if (shouldWarnNullRoleSkip(nullRoleSkipWarnedFocuses, focus.focus_id)) {
|
|
560
|
-
log.warn(`[listener] Pending spawn directive for focus ${focusId.slice(0, 8)} ("${focus.focus_name}") but no assigned_agent_role_id; ` +
|
|
561
|
-
`skipping spawn (coding off-state). Suppressing further per-poll warnings until it resumes.`);
|
|
562
|
-
}
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
// Past the skip paths: this is a genuine lifecycle clear (operatorStopped
|
|
566
|
-
// is false here). The episode is over either way, so clear the marker.
|
|
567
|
-
nullRoleSkipWarnedFocuses.delete(focus.focus_id);
|
|
568
|
-
if (decision.action === 'clear') {
|
|
569
|
-
// Review strand with no resolvable fallback role: drop the directive so
|
|
570
|
-
// the stranded-review re-fire suppression (directive-executor) cannot
|
|
571
|
-
// spin-log forever. review_window_stalled remains the escalation path,
|
|
572
|
-
// and now fires promptly rather than after 6h of churn.
|
|
573
|
-
clearPendingSpawnDirective(focus.focus_id);
|
|
574
|
-
log.warn(`[listener] Stranded review directive for focus ${focusId.slice(0, 8)} ("${focus.focus_name}") has a null assigned_agent_role_id ` +
|
|
575
|
-
`and no seeded '${DEFAULT_REVIEWER_ROLE_NAME}' fallback role resolved -- dropping the directive so it stops re-firing; ` +
|
|
576
|
-
`review_window_stalled will escalate.`);
|
|
577
|
-
continue;
|
|
578
|
-
}
|
|
579
|
-
role = decision.role;
|
|
580
|
-
// Reachable only when operatorStopped is false (an operator stop takes the
|
|
581
|
-
// skip path above), so "(coding team completed)" is a true provenance
|
|
582
|
-
// claim here rather than an assumption stamped on every null-role spawn.
|
|
583
|
-
log.info(`[listener] Review directive for focus ${focusId.slice(0, 8)} ("${focus.focus_name}") has a null assigned_agent_role_id ` +
|
|
584
|
-
`(coding team completed); spawning review with fallback role "${role.name}".`);
|
|
585
|
-
}
|
|
586
|
-
else {
|
|
587
|
-
// Focus has a role again -- any prior null-role blocked episode is over.
|
|
588
|
-
nullRoleSkipWarnedFocuses.delete(focus.focus_id);
|
|
589
|
-
role = roles.get(focus.assigned_agent_role_id);
|
|
590
|
-
if (!role) {
|
|
591
|
-
log.warn(`[listener] Pending spawn directive for focus ${focusId.slice(0, 8)} ("${focus.focus_name}") references unknown role ${focus.assigned_agent_role_id}; ` +
|
|
592
|
-
`skipping spawn.`);
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
// Park focuses with an unhealed team_failure escalation BEFORE attempting
|
|
597
|
-
// the spawn. spawnFocusTeam applies the same guard, but it returns after
|
|
598
|
-
// the guard fires without consuming the pending directive -- so the
|
|
599
|
-
// directive stays queued and this loop re-attempts (and the guard re-logs)
|
|
600
|
-
// every poll. Checking here skips the misleading "Spawning team" log and
|
|
601
|
-
// the wasted spawn call; the directive intentionally stays queued so the
|
|
602
|
-
// (re)review resumes once a human resolves the escalation. The guard
|
|
603
|
-
// throttles its own log to once per blocked episode.
|
|
604
|
-
if (await applyRespawnGuard(focus.focus_id, focus.focus_name)) {
|
|
605
|
-
continue;
|
|
606
|
-
}
|
|
607
|
-
// Spawn cooldown: this path spawns teams for focuses that getReadyFocuses
|
|
608
|
-
// does NOT surface -- focuses with no actionable/queued deliveries (e.g. a
|
|
609
|
-
// reviewing-stage spawn over a focus whose deliveries are all in verify).
|
|
610
|
-
// Such a team idle-exits if its directive can't make progress; without a
|
|
611
|
-
// gate the directive re-fires every poll and the daemon respawns an
|
|
612
|
-
// immediately-idle team every cycle (the reinforcing auto-review/idle-spawn
|
|
613
|
-
// loop). See pending-spawn-guard.ts.
|
|
614
|
-
// Terminated respawn cycle: the spawn/teardown loop for this focus has
|
|
615
|
-
// exhausted its total-duration/count bounds (spawn-cooldown.ts). Respawns are
|
|
616
|
-
// halted permanently (isInSpawnCooldown returns true indefinitely), so the
|
|
617
|
-
// spawn is skipped either way -- but a silent halt is a strand. File the
|
|
618
|
-
// one-per-focus respawn_cycle_exhausted escalation (idempotent, so filing on
|
|
619
|
-
// each poll while terminated yields a single open row) so the terminated
|
|
620
|
-
// cycle is a legible state a human can see and clear.
|
|
621
|
-
if (isRespawnCycleTerminated(focus.focus_id)) {
|
|
622
|
-
await fileRespawnCycleExhaustedEscalation({
|
|
623
|
-
organizationId: config.organizationId,
|
|
624
|
-
focusId: focus.focus_id,
|
|
625
|
-
focusName: focus.focus_name,
|
|
626
|
-
});
|
|
627
|
-
continue;
|
|
628
|
-
}
|
|
629
|
-
if (shouldSkipPendingSpawn(focus.focus_id)) {
|
|
630
|
-
continue;
|
|
631
|
-
}
|
|
632
|
-
if (getActiveTeamCount() >= config.maxTotalSessions) {
|
|
633
|
-
log.info(`Total session limit reached, deferring pending spawn for "${focus.focus_name}"`);
|
|
634
|
-
break;
|
|
635
|
-
}
|
|
636
|
-
const productEntry = focus.product_id ? findProduct(config, focus.product_id) : undefined;
|
|
637
|
-
const spawnConfig = productEntry ? configForProduct(config, productEntry) : config;
|
|
638
|
-
log.info(`[listener] Spawning team for focus "${focus.focus_name}" via pending spawn directive ` +
|
|
639
|
-
`(no actionable deliveries, but directive is queued)`);
|
|
640
|
-
// Audit-phase => read-only spawn (see the processReadyFocuses spawn above).
|
|
641
|
-
// The pending-spawn focus comes from getActiveFocuses, which carries
|
|
642
|
-
// audit_origin + ratified_at, so the second signal (TEL-3) resolves here too.
|
|
643
|
-
await spawnFocusTeam({
|
|
644
|
-
config: spawnConfig,
|
|
645
|
-
focusId: focus.focus_id,
|
|
646
|
-
focusName: focus.focus_name,
|
|
647
|
-
role,
|
|
648
|
-
pipelineConfig: focus.pipeline_config,
|
|
649
|
-
readOnly: isAuditPhaseFocusRow(focus),
|
|
650
|
-
lastClaudeSessionId: null,
|
|
651
|
-
claudeSessionIds: focus.claude_session_ids,
|
|
652
|
-
engineOverride: focus.engine_override,
|
|
653
|
-
localModelOverride: focus.local_model_override,
|
|
654
|
-
orgDefaultEngine: focus.org_default_engine,
|
|
655
|
-
pinnedBranch: focus.branch_name,
|
|
656
|
-
});
|
|
657
|
-
// Record a teardown so repeated spawns through this no-actionable-work path
|
|
658
|
-
// back off exponentially instead of recurring every poll. A team that
|
|
659
|
-
// genuinely makes progress moves the focus into a state getReadyFocuses
|
|
660
|
-
// surfaces (and clearSpawnCooldown can reset it); a team that idle-exits is
|
|
661
|
-
// rate-limited by the growing cooldown. This bounds spawn count on a
|
|
662
|
-
// wedged-in-verify focus. See pending-spawn-guard.ts.
|
|
663
|
-
//
|
|
664
|
-
// ONLY when a team actually spawned (TEL, delivery 5): if spawnFocusTeam
|
|
665
|
-
// SKIPPED (e.g. a terminal focus whose deliveries are all terminal now
|
|
666
|
-
// returns 'skip', or the pre-spawn guards declined), no team exists to tear
|
|
667
|
-
// down, so recording a teardown here would accrue the respawn streak against
|
|
668
|
-
// a focus that correctly spawned nothing -- penalising quiet correctness. The
|
|
669
|
-
// idle-exit backoff is preserved: a genuine spawn leaves hasActiveTeam true at
|
|
670
|
-
// this point (the team runs asynchronously and exits later).
|
|
671
|
-
if (hasActiveTeam(focus.focus_id)) {
|
|
672
|
-
recordPendingSpawnAttempt(focus.focus_id);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
248
|
/**
|
|
677
249
|
* Check that every session recorded as 'starting' or 'running' in the DB
|
|
678
250
|
* has a corresponding active team in memory.
|
|
@@ -689,7 +261,7 @@ export async function checkSessionHealth() {
|
|
|
689
261
|
if (healthCheckConsecutiveFailures > 0) {
|
|
690
262
|
// Use the failure count as the number of cycles to skip
|
|
691
263
|
const skipCycles = Math.min(healthCheckConsecutiveFailures, HEALTH_CHECK_MAX_SKIP_CYCLES);
|
|
692
|
-
healthCheckConsecutiveFailures
|
|
264
|
+
setHealthCheckConsecutiveFailures(healthCheckConsecutiveFailures + 1);
|
|
693
265
|
if (healthCheckConsecutiveFailures % skipCycles !== 0) {
|
|
694
266
|
return; // skip this cycle
|
|
695
267
|
}
|
|
@@ -705,13 +277,13 @@ export async function checkSessionHealth() {
|
|
|
705
277
|
// Reset backoff on success
|
|
706
278
|
if (healthCheckConsecutiveFailures > 0) {
|
|
707
279
|
log.info(`[health] Session health check recovered after ${healthCheckConsecutiveFailures} failures`);
|
|
708
|
-
|
|
709
|
-
|
|
280
|
+
setHealthCheckConsecutiveFailures(0);
|
|
281
|
+
setPollState('idle');
|
|
710
282
|
}
|
|
711
283
|
}
|
|
712
284
|
catch (err) {
|
|
713
|
-
healthCheckConsecutiveFailures
|
|
714
|
-
|
|
285
|
+
setHealthCheckConsecutiveFailures(healthCheckConsecutiveFailures + 1);
|
|
286
|
+
setPollState('backing_off');
|
|
715
287
|
log.warn(`[health] Failed to fetch active sessions (failure #${healthCheckConsecutiveFailures}): ${err.message}`);
|
|
716
288
|
return;
|
|
717
289
|
}
|
|
@@ -796,7 +368,7 @@ export async function checkSessionHealth() {
|
|
|
796
368
|
export async function stopListeners() {
|
|
797
369
|
if (pollInterval) {
|
|
798
370
|
clearInterval(pollInterval);
|
|
799
|
-
|
|
371
|
+
setPollInterval(null);
|
|
800
372
|
}
|
|
801
373
|
log.info('Stopped polling');
|
|
802
374
|
}
|
|
@@ -811,7 +383,7 @@ export async function refreshRoles() {
|
|
|
811
383
|
for (const role of roleList) {
|
|
812
384
|
roles.set(role.id, role);
|
|
813
385
|
}
|
|
814
|
-
|
|
386
|
+
setLastRolesRefreshAt(Date.now());
|
|
815
387
|
log.info(`Refreshed ${roles.size} agent roles`);
|
|
816
388
|
}
|
|
817
389
|
/**
|