create-walle 0.9.25 → 0.9.26
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/README.md +8 -0
- package/bin/create-walle.js +815 -45
- package/package.json +2 -2
- package/template/bin/ctm-dev-cleanup.js +90 -4
- package/template/bin/ctm-launch.sh +49 -1
- package/template/bin/dev.sh +45 -1
- package/template/bin/ensure-stable-node.js +132 -0
- package/template/bin/install-service.sh +9 -0
- package/template/claude-task-manager/api-prompts.js +899 -119
- package/template/claude-task-manager/approval-agent.js +360 -40
- package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
- package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
- package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
- package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
- package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
- package/template/claude-task-manager/db.js +399 -48
- package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
- package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
- package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
- package/template/claude-task-manager/lib/approval-hook.js +200 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
- package/template/claude-task-manager/lib/auth-rules.js +11 -0
- package/template/claude-task-manager/lib/background-llm.js +32 -4
- package/template/claude-task-manager/lib/codesign-identity.js +140 -0
- package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
- package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
- package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
- package/template/claude-task-manager/lib/codex-paths.js +73 -0
- package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
- package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
- package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
- package/template/claude-task-manager/lib/command-targets.js +163 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
- package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
- package/template/claude-task-manager/lib/escalation-review.js +80 -3
- package/template/claude-task-manager/lib/flow-control.js +52 -0
- package/template/claude-task-manager/lib/fs-watcher.js +24 -15
- package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
- package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
- package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
- package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
- package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
- package/template/claude-task-manager/lib/perf-tracker.js +29 -2
- package/template/claude-task-manager/lib/permission-match.js +146 -16
- package/template/claude-task-manager/lib/project-slug.js +33 -0
- package/template/claude-task-manager/lib/prompt-intent.js +51 -4
- package/template/claude-task-manager/lib/read-pool-client.js +48 -3
- package/template/claude-task-manager/lib/real-node.js +73 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
- package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
- package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
- package/template/claude-task-manager/lib/session-history.js +5 -7
- package/template/claude-task-manager/lib/session-host-manager.js +19 -0
- package/template/claude-task-manager/lib/session-jobs.js +6 -0
- package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
- package/template/claude-task-manager/lib/session-messages-page.js +211 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
- package/template/claude-task-manager/lib/session-standup.js +8 -0
- package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
- package/template/claude-task-manager/lib/session-token-usage.js +30 -8
- package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
- package/template/claude-task-manager/lib/storage-migration.js +2 -1
- package/template/claude-task-manager/lib/transcript-store.js +179 -12
- package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
- package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
- package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
- package/template/claude-task-manager/package.json +5 -2
- package/template/claude-task-manager/prompt-harvest.js +31 -11
- package/template/claude-task-manager/providers/claude-code.js +29 -1
- package/template/claude-task-manager/providers/codex.js +13 -1
- package/template/claude-task-manager/public/css/setup.css +11 -0
- package/template/claude-task-manager/public/css/walle-session.css +132 -4
- package/template/claude-task-manager/public/css/walle.css +89 -0
- package/template/claude-task-manager/public/icon-16.png +0 -0
- package/template/claude-task-manager/public/icon-32.png +0 -0
- package/template/claude-task-manager/public/icon-512.png +0 -0
- package/template/claude-task-manager/public/index.html +2483 -165
- package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
- package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
- package/template/claude-task-manager/public/js/message-renderer.js +60 -1
- package/template/claude-task-manager/public/js/prompts.js +13 -1
- package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
- package/template/claude-task-manager/public/js/setup.js +54 -10
- package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
- package/template/claude-task-manager/public/js/stream-view.js +78 -0
- package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
- package/template/claude-task-manager/public/js/tool-state.js +155 -0
- package/template/claude-task-manager/public/js/walle-session.js +887 -326
- package/template/claude-task-manager/public/js/walle.js +306 -195
- package/template/claude-task-manager/public/m/app.css +1 -0
- package/template/claude-task-manager/public/m/app.js +33 -3
- package/template/claude-task-manager/queue-engine.js +45 -1
- package/template/claude-task-manager/server.js +3367 -540
- package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
- package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
- package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
- package/template/claude-task-manager/workers/session-host-process.js +10 -0
- package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
- package/template/package.json +2 -3
- package/template/shared/icons/AppIcon-ctm.icns +0 -0
- package/template/shared/icons/AppIcon-walle.icns +0 -0
- package/template/wall-e/agent.js +139 -18
- package/template/wall-e/api-walle.js +201 -22
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
- package/template/wall-e/brain.js +1049 -39
- package/template/wall-e/chat.js +427 -86
- package/template/wall-e/coding/acceptance-contract.js +26 -1
- package/template/wall-e/coding/action-memory-policy.js +353 -0
- package/template/wall-e/coding/action-memory-store.js +814 -0
- package/template/wall-e/coding/initial-messages.js +197 -0
- package/template/wall-e/coding/no-progress-guard.js +327 -0
- package/template/wall-e/coding/permission-service.js +88 -22
- package/template/wall-e/coding/session-workspaces.js +81 -0
- package/template/wall-e/coding/shell-sandbox.js +124 -0
- package/template/wall-e/coding/stream-processor.js +63 -2
- package/template/wall-e/coding/tool-execution-controller.js +14 -1
- package/template/wall-e/coding/tool-registry.js +1 -1
- package/template/wall-e/coding/transcript-writer.js +3 -0
- package/template/wall-e/coding-orchestrator.js +636 -35
- package/template/wall-e/coding-prompts.js +51 -2
- package/template/wall-e/docs/model-routing-policy.md +59 -0
- package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
- package/template/wall-e/extraction/knowledge-extractor.js +76 -23
- package/template/wall-e/http/chat-api.js +30 -12
- package/template/wall-e/http/model-admin.js +93 -1
- package/template/wall-e/lib/background-lanes.js +133 -0
- package/template/wall-e/lib/boot-profile.js +11 -0
- package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
- package/template/wall-e/lib/brain-read-pool-client.js +311 -0
- package/template/wall-e/lib/diagnostics-flags.js +87 -0
- package/template/wall-e/lib/event-loop-monitor.js +74 -3
- package/template/wall-e/lib/mcp-integration.js +7 -1
- package/template/wall-e/lib/real-node.js +98 -0
- package/template/wall-e/lib/runtime-health.js +206 -0
- package/template/wall-e/lib/runtime-worker-pool.js +101 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
- package/template/wall-e/lib/scheduler.js +446 -17
- package/template/wall-e/lib/service-health.js +61 -2
- package/template/wall-e/lib/service-readiness.js +258 -0
- package/template/wall-e/lib/usage.js +152 -0
- package/template/wall-e/lib/worker-thread-pool.js +389 -0
- package/template/wall-e/llm/client.js +81 -4
- package/template/wall-e/llm/default-fallback.js +54 -8
- package/template/wall-e/llm/mlx.js +536 -73
- package/template/wall-e/llm/mlx.plugin.json +1 -1
- package/template/wall-e/llm/ollama.js +342 -43
- package/template/wall-e/llm/provider-error.js +18 -1
- package/template/wall-e/llm/provider-health-state.js +176 -0
- package/template/wall-e/llm/routing-policy.js +796 -0
- package/template/wall-e/llm/supported-models.js +5 -0
- package/template/wall-e/loops/tasks.js +60 -14
- package/template/wall-e/loops/think.js +89 -24
- package/template/wall-e/mcp-server.js +192 -28
- package/template/wall-e/server.js +32 -7
- package/template/wall-e/skills/script-skill-runner.js +8 -1
- package/template/wall-e/skills/skill-planner.js +64 -1
- package/template/wall-e/tools/builtin-middleware.js +67 -2
- package/template/wall-e/tools/local-tools.js +116 -26
- package/template/wall-e/tools/permission-checker.js +52 -4
- package/template/wall-e/tools/permission-rules.js +36 -0
- package/template/wall-e/tools/shell-analyzer.js +46 -1
- package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
- package/template/wall-e/training/real-trajectory-miner.js +2617 -0
- package/template/wall-e/training/replay-eval-analysis.js +151 -0
- package/template/wall-e/training/run-shell-command-selector.js +277 -0
- package/template/wall-e/training/tool-sft-dataset.js +312 -0
- package/template/wall-e/training/tool-sft-renderers.js +144 -0
- package/template/wall-e/training/tool-trace-harvester.js +1440 -0
- package/template/wall-e/training/trajectory-action-selector.js +364 -0
- package/template/wall-e/weather-runtime.js +232 -0
- package/template/wall-e/workers/brain-owner-worker.js +162 -0
- package/template/wall-e/workers/brain-read-worker.js +148 -0
- package/template/wall-e/workers/runtime-worker.js +145 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const EventEmitter = require('events');
|
|
3
|
+
const runtimeHealth = require('./runtime-health');
|
|
4
|
+
const { runtimeDiagnosticsEnabled, perfLogsEnabled } = require('./diagnostics-flags');
|
|
3
5
|
|
|
4
6
|
let _instance = null;
|
|
5
7
|
|
|
@@ -24,6 +26,9 @@ const MIN_REFIRE_GAP_MS = 2_000;
|
|
|
24
26
|
*/
|
|
25
27
|
const DEFAULT_MISSED_JOB_STAGGER_MS = 5_000;
|
|
26
28
|
const DEFAULT_MAX_MISSED_JOBS_PER_RESTART = 5;
|
|
29
|
+
const DEFAULT_SLOW_TICK_MS = 250;
|
|
30
|
+
const DEFAULT_SLOW_JOB_MS = 2_000;
|
|
31
|
+
const PERF_LOG_THROTTLE_MS = 30_000;
|
|
27
32
|
|
|
28
33
|
/** Default cascade check: true if any numeric value in result is > 0 */
|
|
29
34
|
function _defaultCascadeCheck(result) {
|
|
@@ -39,6 +44,8 @@ class Scheduler extends EventEmitter {
|
|
|
39
44
|
* @param {number} [opts.tickMs=5000]
|
|
40
45
|
* @param {Object} [opts.pools] - Resource pool limits: { llm: 2, ollama: 1, ... }
|
|
41
46
|
* @param {number} [opts.shutdownTimeoutMs=5000]
|
|
47
|
+
* @param {Object} [opts.workerPool] - Optional WorkerThreadPool for off-thread job bodies
|
|
48
|
+
* @param {number} [opts.defaultWorkerJobTimeoutMs=0] - Default timeout for worker-backed jobs
|
|
42
49
|
*/
|
|
43
50
|
constructor(opts = {}) {
|
|
44
51
|
super();
|
|
@@ -68,6 +75,12 @@ class Scheduler extends EventEmitter {
|
|
|
68
75
|
this._persistBrain = opts.persistBrain || null;
|
|
69
76
|
this._missedJobStaggerMs = opts.missedJobStaggerMs ?? DEFAULT_MISSED_JOB_STAGGER_MS;
|
|
70
77
|
this._maxMissedJobsPerRestart = opts.maxMissedJobsPerRestart ?? DEFAULT_MAX_MISSED_JOBS_PER_RESTART;
|
|
78
|
+
this._workerPool = opts.workerPool || null;
|
|
79
|
+
this._closeWorkerPoolOnShutdown = opts.closeWorkerPoolOnShutdown === true;
|
|
80
|
+
this._workerJobNames = new Set(Array.isArray(opts.workerJobs) ? opts.workerJobs : []);
|
|
81
|
+
this._defaultWorkerJobTimeoutMs = Number.isFinite(Number(opts.defaultWorkerJobTimeoutMs))
|
|
82
|
+
? Math.max(0, Math.trunc(Number(opts.defaultWorkerJobTimeoutMs)))
|
|
83
|
+
: 0;
|
|
71
84
|
|
|
72
85
|
this._poolLimits = {
|
|
73
86
|
llm: 2,
|
|
@@ -85,6 +98,25 @@ class Scheduler extends EventEmitter {
|
|
|
85
98
|
for (const pool of Object.keys(this._poolLimits)) {
|
|
86
99
|
this._poolActive.set(pool, new Set());
|
|
87
100
|
}
|
|
101
|
+
|
|
102
|
+
this._metrics = {
|
|
103
|
+
startedAt: Date.now(),
|
|
104
|
+
tickCount: 0,
|
|
105
|
+
tickErrorCount: 0,
|
|
106
|
+
dispatchCount: 0,
|
|
107
|
+
completionCount: 0,
|
|
108
|
+
errorCount: 0,
|
|
109
|
+
timeoutCount: 0,
|
|
110
|
+
poolFullCount: 0,
|
|
111
|
+
maxTickDurationMs: 0,
|
|
112
|
+
lastTickAt: null,
|
|
113
|
+
lastTickDurationMs: null,
|
|
114
|
+
lastTickSummary: null,
|
|
115
|
+
lastError: null,
|
|
116
|
+
ineligibleCounts: {},
|
|
117
|
+
jobs: new Map(),
|
|
118
|
+
perfLogLastAt: new Map(),
|
|
119
|
+
};
|
|
88
120
|
}
|
|
89
121
|
|
|
90
122
|
static getInstance() { return _instance; }
|
|
@@ -107,6 +139,9 @@ class Scheduler extends EventEmitter {
|
|
|
107
139
|
* @param {Function} [desc.shouldRun] - () => boolean guard
|
|
108
140
|
* @param {Function} [desc.onResult] - (result) => void
|
|
109
141
|
* @param {Function} [desc.onError] - (err) => void
|
|
142
|
+
* @param {boolean} [desc.worker] - run body through opts.workerPool
|
|
143
|
+
* @param {string} [desc.workerJobName] - worker-side job name (defaults to desc.name)
|
|
144
|
+
* @param {boolean} [desc.workerFallbackLocal=true] - fallback to desc.run if worker unavailable
|
|
110
145
|
* @returns {Scheduler}
|
|
111
146
|
*/
|
|
112
147
|
register(desc) {
|
|
@@ -114,7 +149,15 @@ class Scheduler extends EventEmitter {
|
|
|
114
149
|
throw new Error('Job requires name, run, and intervalMs');
|
|
115
150
|
}
|
|
116
151
|
// Clone to avoid mutating caller's object
|
|
117
|
-
desc = {
|
|
152
|
+
desc = {
|
|
153
|
+
priority: 5,
|
|
154
|
+
enabled: true,
|
|
155
|
+
worker: this._workerJobNames.has(desc.name),
|
|
156
|
+
...desc,
|
|
157
|
+
};
|
|
158
|
+
if (desc.worker && !desc.timeoutMs && this._defaultWorkerJobTimeoutMs > 0) {
|
|
159
|
+
desc.timeoutMs = this._defaultWorkerJobTimeoutMs;
|
|
160
|
+
}
|
|
118
161
|
|
|
119
162
|
const now = Date.now();
|
|
120
163
|
const initialState = {
|
|
@@ -333,6 +376,10 @@ class Scheduler extends EventEmitter {
|
|
|
333
376
|
pool: desc.pool || null,
|
|
334
377
|
priority: desc.priority,
|
|
335
378
|
intervalMs: desc.intervalMs,
|
|
379
|
+
timeoutMs: desc.timeoutMs || null,
|
|
380
|
+
workerTimeoutMs: desc.workerTimeoutMs || null,
|
|
381
|
+
worker: !!desc.worker,
|
|
382
|
+
workerJobName: desc.workerJobName || null,
|
|
336
383
|
};
|
|
337
384
|
}
|
|
338
385
|
|
|
@@ -351,13 +398,181 @@ class Scheduler extends EventEmitter {
|
|
|
351
398
|
queued,
|
|
352
399
|
};
|
|
353
400
|
}
|
|
401
|
+
if (this._workerPool && typeof this._workerPool.getStatus === 'function') {
|
|
402
|
+
result.worker_threads = this._workerPool.getStatus();
|
|
403
|
+
}
|
|
354
404
|
return result;
|
|
355
405
|
}
|
|
356
406
|
|
|
407
|
+
getActiveSnapshot() {
|
|
408
|
+
const jobs = [];
|
|
409
|
+
for (const [name, job] of this._jobs) {
|
|
410
|
+
if (job.state.status === 'running' || job.state.status === 'queued') {
|
|
411
|
+
jobs.push({
|
|
412
|
+
name,
|
|
413
|
+
status: job.state.status,
|
|
414
|
+
pool: job.descriptor.pool || null,
|
|
415
|
+
worker: !!job.descriptor.worker,
|
|
416
|
+
workerTimeoutMs: job.descriptor.workerTimeoutMs ?? job.descriptor.timeoutMs ?? null,
|
|
417
|
+
age_ms: job.state.lastRunAt ? Math.max(0, Date.now() - job.state.lastRunAt) : 0,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
jobs,
|
|
423
|
+
pools: this.getPoolState(),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
357
427
|
uptime() {
|
|
358
428
|
return this._startedAt ? Date.now() - this._startedAt : 0;
|
|
359
429
|
}
|
|
360
430
|
|
|
431
|
+
_metricsEnabled() {
|
|
432
|
+
return runtimeDiagnosticsEnabled();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
_safeMetricName(name) {
|
|
436
|
+
return String(name || 'job')
|
|
437
|
+
.replace(/[^a-zA-Z0-9_.:-]/g, '_')
|
|
438
|
+
.slice(0, 80) || 'job';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
_maybeLogSlow(kind, name, durationMs, thresholdMs, meta = {}) {
|
|
442
|
+
if (!perfLogsEnabled() || durationMs < thresholdMs) return;
|
|
443
|
+
const key = `${kind}:${name}`;
|
|
444
|
+
const now = Date.now();
|
|
445
|
+
const last = this._metrics.perfLogLastAt.get(key) || 0;
|
|
446
|
+
if (now - last < PERF_LOG_THROTTLE_MS) return;
|
|
447
|
+
this._metrics.perfLogLastAt.set(key, now);
|
|
448
|
+
const safeMeta = {};
|
|
449
|
+
for (const [k, v] of Object.entries(meta || {})) {
|
|
450
|
+
if (typeof v === 'number' || typeof v === 'boolean') safeMeta[k] = v;
|
|
451
|
+
else if (typeof v === 'string') safeMeta[k] = v.slice(0, 80);
|
|
452
|
+
}
|
|
453
|
+
console.warn(`[scheduler-perf] ${kind} ${name} took ${durationMs}ms ${JSON.stringify(safeMeta)}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
_recordTickMetric(summary, durationMs, ok = true, err = null) {
|
|
457
|
+
if (!this._metricsEnabled()) return;
|
|
458
|
+
const meta = {
|
|
459
|
+
jobs: summary?.jobs || 0,
|
|
460
|
+
candidates: summary?.candidates || 0,
|
|
461
|
+
dispatched: summary?.dispatched || 0,
|
|
462
|
+
queued: summary?.queued || 0,
|
|
463
|
+
poolFull: summary?.poolFull || 0,
|
|
464
|
+
watchdogReset: summary?.watchdogReset || 0,
|
|
465
|
+
resetQueued: summary?.resetQueued || 0,
|
|
466
|
+
};
|
|
467
|
+
this._metrics.tickCount++;
|
|
468
|
+
if (!ok) this._metrics.tickErrorCount++;
|
|
469
|
+
this._metrics.lastTickAt = new Date().toISOString();
|
|
470
|
+
this._metrics.lastTickDurationMs = durationMs;
|
|
471
|
+
this._metrics.maxTickDurationMs = Math.max(this._metrics.maxTickDurationMs || 0, durationMs);
|
|
472
|
+
this._metrics.lastTickSummary = summary || null;
|
|
473
|
+
this._metrics.lastError = err ? (err.message || String(err)).slice(0, 160) : null;
|
|
474
|
+
if (summary?.ineligible) {
|
|
475
|
+
for (const [reason, count] of Object.entries(summary.ineligible)) {
|
|
476
|
+
this._metrics.ineligibleCounts[reason] = (this._metrics.ineligibleCounts[reason] || 0) + count;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
this._metrics.poolFullCount += summary?.poolFull || 0;
|
|
480
|
+
runtimeHealth.recordOperation('scheduler.tick', durationMs, {
|
|
481
|
+
ok,
|
|
482
|
+
error: err ? (err.code || err.name || 'error') : undefined,
|
|
483
|
+
meta,
|
|
484
|
+
});
|
|
485
|
+
this._maybeLogSlow('tick', 'scheduler.tick', durationMs, DEFAULT_SLOW_TICK_MS, meta);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
_recordDispatchMetric() {
|
|
489
|
+
if (!this._metricsEnabled()) return;
|
|
490
|
+
this._metrics.dispatchCount++;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
_recordJobMetric(name, durationMs, ok = true, err = null, meta = {}) {
|
|
494
|
+
if (!this._metricsEnabled()) return;
|
|
495
|
+
const safeName = this._safeMetricName(name);
|
|
496
|
+
let bucket = this._metrics.jobs.get(safeName);
|
|
497
|
+
if (!bucket) {
|
|
498
|
+
bucket = {
|
|
499
|
+
name: safeName,
|
|
500
|
+
count: 0,
|
|
501
|
+
failed: 0,
|
|
502
|
+
totalMs: 0,
|
|
503
|
+
maxMs: 0,
|
|
504
|
+
lastDurationMs: 0,
|
|
505
|
+
lastAt: null,
|
|
506
|
+
lastError: null,
|
|
507
|
+
lastMeta: {},
|
|
508
|
+
};
|
|
509
|
+
this._metrics.jobs.set(safeName, bucket);
|
|
510
|
+
}
|
|
511
|
+
bucket.count++;
|
|
512
|
+
bucket.totalMs += durationMs;
|
|
513
|
+
bucket.maxMs = Math.max(bucket.maxMs, durationMs);
|
|
514
|
+
bucket.lastDurationMs = durationMs;
|
|
515
|
+
bucket.lastAt = new Date().toISOString();
|
|
516
|
+
bucket.lastMeta = { ...meta };
|
|
517
|
+
if (!ok) {
|
|
518
|
+
bucket.failed++;
|
|
519
|
+
bucket.lastError = err ? (err.message || err.code || err.name || String(err)).slice(0, 160) : 'error';
|
|
520
|
+
this._metrics.errorCount++;
|
|
521
|
+
} else {
|
|
522
|
+
this._metrics.completionCount++;
|
|
523
|
+
bucket.lastError = null;
|
|
524
|
+
}
|
|
525
|
+
this._maybeLogSlow('job', safeName, durationMs, DEFAULT_SLOW_JOB_MS, meta);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
getMetrics(options = {}) {
|
|
529
|
+
if (!this._metricsEnabled()) {
|
|
530
|
+
return {
|
|
531
|
+
enabled: false,
|
|
532
|
+
uptimeMs: this.uptime(),
|
|
533
|
+
jobsRegistered: this._jobs.size,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
const jobLimit = Math.max(1, Math.min(100, Number(options.jobLimit) || 20));
|
|
537
|
+
const jobs = Array.from(this._metrics.jobs.values())
|
|
538
|
+
.map((job) => ({
|
|
539
|
+
name: job.name,
|
|
540
|
+
count: job.count,
|
|
541
|
+
failed: job.failed,
|
|
542
|
+
avgMs: job.count ? Math.round(job.totalMs / job.count) : 0,
|
|
543
|
+
maxMs: job.maxMs,
|
|
544
|
+
lastDurationMs: job.lastDurationMs,
|
|
545
|
+
lastAt: job.lastAt,
|
|
546
|
+
lastError: job.lastError,
|
|
547
|
+
lastMeta: job.lastMeta || {},
|
|
548
|
+
}))
|
|
549
|
+
.sort((a, b) => b.maxMs - a.maxMs || b.count - a.count)
|
|
550
|
+
.slice(0, jobLimit);
|
|
551
|
+
return {
|
|
552
|
+
enabled: true,
|
|
553
|
+
uptimeMs: this.uptime(),
|
|
554
|
+
startedAt: this._startedAt ? new Date(this._startedAt).toISOString() : null,
|
|
555
|
+
tickCount: this._metrics.tickCount,
|
|
556
|
+
tickErrorCount: this._metrics.tickErrorCount,
|
|
557
|
+
dispatchCount: this._metrics.dispatchCount,
|
|
558
|
+
completionCount: this._metrics.completionCount,
|
|
559
|
+
errorCount: this._metrics.errorCount,
|
|
560
|
+
timeoutCount: this._metrics.timeoutCount,
|
|
561
|
+
poolFullCount: this._metrics.poolFullCount,
|
|
562
|
+
maxTickDurationMs: this._metrics.maxTickDurationMs,
|
|
563
|
+
lastTickAt: this._metrics.lastTickAt,
|
|
564
|
+
lastTickDurationMs: this._metrics.lastTickDurationMs,
|
|
565
|
+
lastTickSummary: this._metrics.lastTickSummary,
|
|
566
|
+
lastError: this._metrics.lastError,
|
|
567
|
+
ineligibleCounts: { ...this._metrics.ineligibleCounts },
|
|
568
|
+
activeJobs: this.getActiveSnapshot(),
|
|
569
|
+
jobs,
|
|
570
|
+
workerPool: this._workerPool && typeof this._workerPool.getStatus === 'function'
|
|
571
|
+
? this._workerPool.getStatus()
|
|
572
|
+
: null,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
361
576
|
async shutdown() {
|
|
362
577
|
if (this._shuttingDown) return;
|
|
363
578
|
this._shuttingDown = true;
|
|
@@ -397,27 +612,50 @@ class Scheduler extends EventEmitter {
|
|
|
397
612
|
}
|
|
398
613
|
|
|
399
614
|
this.emit('shutdown');
|
|
615
|
+
if (this._workerPool && this._closeWorkerPoolOnShutdown && typeof this._workerPool.close === 'function') {
|
|
616
|
+
try { await this._workerPool.close({ timeoutMs: this._shutdownTimeoutMs }); } catch {}
|
|
617
|
+
}
|
|
400
618
|
}
|
|
401
619
|
|
|
402
620
|
_tick() {
|
|
403
621
|
if (this._shuttingDown) return;
|
|
404
622
|
|
|
623
|
+
const startedAt = Date.now();
|
|
624
|
+
let summary = null;
|
|
405
625
|
try {
|
|
406
|
-
this._tickInner();
|
|
626
|
+
summary = this._tickInner();
|
|
627
|
+
this._recordTickMetric(summary, Date.now() - startedAt, true);
|
|
628
|
+
return summary;
|
|
407
629
|
} catch (err) {
|
|
630
|
+
this._recordTickMetric(summary, Date.now() - startedAt, false, err);
|
|
408
631
|
// Log and recover — don't let a single bad tick kill the scheduler loop.
|
|
409
632
|
// setInterval keeps firing, so the next tick will retry.
|
|
410
633
|
console.error('[scheduler] Tick error (recovering):', err);
|
|
411
634
|
this.emit('tickError', err);
|
|
635
|
+
return null;
|
|
412
636
|
}
|
|
413
637
|
}
|
|
414
638
|
|
|
415
639
|
_tickInner() {
|
|
416
640
|
const now = Date.now();
|
|
641
|
+
const summary = {
|
|
642
|
+
jobs: this._jobs.size,
|
|
643
|
+
resetQueued: 0,
|
|
644
|
+
watchdogReset: 0,
|
|
645
|
+
candidates: 0,
|
|
646
|
+
dispatched: 0,
|
|
647
|
+
queued: 0,
|
|
648
|
+
poolFull: 0,
|
|
649
|
+
ineligible: {},
|
|
650
|
+
poolFullByPool: {},
|
|
651
|
+
};
|
|
417
652
|
|
|
418
653
|
// 0. Reset queued→idle so they're reconsidered this tick
|
|
419
654
|
for (const [, job] of this._jobs) {
|
|
420
|
-
if (job.state.status === 'queued')
|
|
655
|
+
if (job.state.status === 'queued') {
|
|
656
|
+
job.state.status = 'idle';
|
|
657
|
+
summary.resetQueued++;
|
|
658
|
+
}
|
|
421
659
|
}
|
|
422
660
|
|
|
423
661
|
// 1. Watchdog: check for stuck jobs
|
|
@@ -426,6 +664,7 @@ class Scheduler extends EventEmitter {
|
|
|
426
664
|
if (now - job.state.lastRunAt > job.descriptor.timeoutMs) {
|
|
427
665
|
console.error(`[scheduler] Job "${name}" stuck for ${now - job.state.lastRunAt}ms, force-resetting`);
|
|
428
666
|
this._forceReset(name);
|
|
667
|
+
summary.watchdogReset++;
|
|
429
668
|
}
|
|
430
669
|
}
|
|
431
670
|
}
|
|
@@ -433,22 +672,44 @@ class Scheduler extends EventEmitter {
|
|
|
433
672
|
// 2. Collect due candidates (idle, past nextEligibleAt, dependencies met, guard passes)
|
|
434
673
|
const candidates = [];
|
|
435
674
|
for (const [, job] of this._jobs) {
|
|
436
|
-
|
|
675
|
+
const reason = this._ineligibleReason(job, now);
|
|
676
|
+
if (reason) {
|
|
677
|
+
summary.ineligible[reason] = (summary.ineligible[reason] || 0) + 1;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
437
680
|
candidates.push(job);
|
|
438
681
|
}
|
|
682
|
+
summary.candidates = candidates.length;
|
|
439
683
|
|
|
440
684
|
// 3. Sort by priority (lower = higher priority)
|
|
441
685
|
candidates.sort((a, b) => a.descriptor.priority - b.descriptor.priority);
|
|
442
686
|
|
|
443
|
-
// 4. Dispatch respecting pool limits
|
|
687
|
+
// 4. Dispatch respecting pool limits. Worker-thread requests register
|
|
688
|
+
// asynchronously inside WorkerThreadSlot.request(), so reserve the slots we
|
|
689
|
+
// consume during this tick to avoid oversubscribing the pool before the
|
|
690
|
+
// parent-side pending counters catch up.
|
|
691
|
+
let workerSlotsRemaining = this._workerPoolCapacityAvailable();
|
|
444
692
|
for (const job of candidates) {
|
|
445
693
|
const pool = job.descriptor.pool;
|
|
446
694
|
if (pool && !this._poolHasCapacity(pool)) {
|
|
447
695
|
job.state.status = 'queued';
|
|
696
|
+
summary.queued++;
|
|
697
|
+
summary.poolFull++;
|
|
698
|
+
summary.poolFullByPool[pool] = (summary.poolFullByPool[pool] || 0) + 1;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (job.descriptor.worker && this._workerPool && workerSlotsRemaining <= 0) {
|
|
702
|
+
job.state.status = 'queued';
|
|
703
|
+
summary.queued++;
|
|
704
|
+
summary.poolFull++;
|
|
705
|
+
summary.poolFullByPool.worker_threads = (summary.poolFullByPool.worker_threads || 0) + 1;
|
|
448
706
|
continue;
|
|
449
707
|
}
|
|
708
|
+
if (job.descriptor.worker && this._workerPool) workerSlotsRemaining--;
|
|
450
709
|
this._dispatch(job);
|
|
710
|
+
summary.dispatched++;
|
|
451
711
|
}
|
|
712
|
+
return summary;
|
|
452
713
|
}
|
|
453
714
|
|
|
454
715
|
_poolHasCapacity(pool) {
|
|
@@ -458,6 +719,37 @@ class Scheduler extends EventEmitter {
|
|
|
458
719
|
return active.size < limit;
|
|
459
720
|
}
|
|
460
721
|
|
|
722
|
+
_workerPoolHasCapacity() {
|
|
723
|
+
return this._workerPoolCapacityAvailable() > 0;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
_workerPoolCapacityAvailable() {
|
|
727
|
+
if (!this._workerPool || typeof this._workerPool.getStatus !== 'function') return Number.POSITIVE_INFINITY;
|
|
728
|
+
try {
|
|
729
|
+
const status = this._workerPool.getStatus();
|
|
730
|
+
const size = Number(status?.size) || (Array.isArray(status?.workers) ? status.workers.length : 0);
|
|
731
|
+
if (size <= 0) return Number.POSITIVE_INFINITY;
|
|
732
|
+
const queuedOrRunning = Number.isFinite(Number(status?.queuedOrRunning))
|
|
733
|
+
? Number(status.queuedOrRunning)
|
|
734
|
+
: (Array.isArray(status?.workers)
|
|
735
|
+
? status.workers.reduce((sum, worker) => {
|
|
736
|
+
return sum
|
|
737
|
+
+ (Number(worker.pendingClientRequests) || 0)
|
|
738
|
+
+ (worker.active ? 1 : 0)
|
|
739
|
+
+ (Number(worker.pending) || 0);
|
|
740
|
+
}, 0)
|
|
741
|
+
: 0);
|
|
742
|
+
return Math.max(0, size - queuedOrRunning);
|
|
743
|
+
} catch {
|
|
744
|
+
return Number.POSITIVE_INFINITY;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
_markWorkerPoolQueued(job, skipped) {
|
|
749
|
+
job.state.status = 'queued';
|
|
750
|
+
skipped.push({ job, reason: 'worker-pool-full' });
|
|
751
|
+
}
|
|
752
|
+
|
|
461
753
|
_dependenciesMet(descriptor) {
|
|
462
754
|
if (!descriptor.dependsOn || descriptor.dependsOn.length === 0) return true;
|
|
463
755
|
return descriptor.dependsOn.every(dep => {
|
|
@@ -515,6 +807,32 @@ class Scheduler extends EventEmitter {
|
|
|
515
807
|
job.state.status = 'running';
|
|
516
808
|
job.state.lastRunAt = Date.now();
|
|
517
809
|
const epoch = job._epoch; // capture epoch to detect force-reset
|
|
810
|
+
const startedAt = Date.now();
|
|
811
|
+
const trigger = job._wakeTrigger || 'interval';
|
|
812
|
+
const safeName = this._safeMetricName(name);
|
|
813
|
+
const workerTimeoutMs = job.descriptor.worker
|
|
814
|
+
? (job.descriptor.workerTimeoutMs ?? job.descriptor.timeoutMs ?? this._defaultWorkerJobTimeoutMs ?? 0)
|
|
815
|
+
: 0;
|
|
816
|
+
const op = runtimeHealth.beginOperation(`scheduler.job.${safeName}`, {
|
|
817
|
+
pool: pool || 'none',
|
|
818
|
+
trigger,
|
|
819
|
+
worker_requested: !!job.descriptor.worker,
|
|
820
|
+
...(workerTimeoutMs > 0 ? { worker_timeout_ms: workerTimeoutMs } : {}),
|
|
821
|
+
});
|
|
822
|
+
let failed = false;
|
|
823
|
+
let failure = null;
|
|
824
|
+
let workerMeta = {
|
|
825
|
+
worker_requested: !!job.descriptor.worker,
|
|
826
|
+
worker_used: false,
|
|
827
|
+
worker_fallback: false,
|
|
828
|
+
};
|
|
829
|
+
this._recordDispatchMetric();
|
|
830
|
+
job._activeRun = {
|
|
831
|
+
op,
|
|
832
|
+
startedAt,
|
|
833
|
+
trigger,
|
|
834
|
+
workerMeta,
|
|
835
|
+
};
|
|
518
836
|
|
|
519
837
|
if (pool) this._poolActive.get(pool)?.add(name);
|
|
520
838
|
|
|
@@ -531,10 +849,7 @@ class Scheduler extends EventEmitter {
|
|
|
531
849
|
if (overrides && overrides[name]) modelOverride = overrides[name];
|
|
532
850
|
}
|
|
533
851
|
} catch {}
|
|
534
|
-
const result = await
|
|
535
|
-
trigger: job._wakeTrigger || 'interval',
|
|
536
|
-
modelOverride: modelOverride || null,
|
|
537
|
-
});
|
|
852
|
+
const result = await this._runJob(job, { trigger, modelOverride: modelOverride || null }, workerMeta);
|
|
538
853
|
// If force-reset happened while we were running, this promise is stale — bail
|
|
539
854
|
if (job._epoch !== epoch) return;
|
|
540
855
|
job._wakeTrigger = null;
|
|
@@ -570,6 +885,8 @@ class Scheduler extends EventEmitter {
|
|
|
570
885
|
this._checkCascades(name, result);
|
|
571
886
|
} catch (err) {
|
|
572
887
|
if (job._epoch !== epoch) return; // stale after force-reset
|
|
888
|
+
failed = true;
|
|
889
|
+
failure = err;
|
|
573
890
|
job.state.lastError = err.message;
|
|
574
891
|
job.state.errorCount++;
|
|
575
892
|
|
|
@@ -580,11 +897,23 @@ class Scheduler extends EventEmitter {
|
|
|
580
897
|
if (onError) onError(err);
|
|
581
898
|
this.emit('jobError', name, err);
|
|
582
899
|
} finally {
|
|
583
|
-
|
|
900
|
+
const stale = job._epoch !== epoch;
|
|
901
|
+
const finishedAt = Date.now();
|
|
902
|
+
const durationMs = finishedAt - startedAt;
|
|
903
|
+
const metricMeta = { pool: pool || 'none', trigger, stale, ...workerMeta };
|
|
904
|
+
const phaseMeta = this._workerPhaseMetric(job.state.lastResult);
|
|
905
|
+
Object.assign(metricMeta, phaseMeta);
|
|
906
|
+
const metricError = failed ? failure : (stale ? new Error('stale_epoch') : null);
|
|
907
|
+
if (job._activeRun?.op === op) {
|
|
908
|
+
op.end({ ok: !failed && !stale, error: metricError, meta: metricMeta });
|
|
909
|
+
job._activeRun = null;
|
|
910
|
+
this._recordJobMetric(name, durationMs, !failed && !stale, metricError, metricMeta);
|
|
911
|
+
}
|
|
912
|
+
if (stale) return; // stale — _forceReset already cleaned up
|
|
584
913
|
// Job may have been unregistered by self-stop
|
|
585
914
|
if (this._jobs.has(name)) {
|
|
586
915
|
job.state.status = 'idle';
|
|
587
|
-
job.state.lastFinishedAt =
|
|
916
|
+
job.state.lastFinishedAt = finishedAt;
|
|
588
917
|
job.state.lastDurationMs = job.state.lastFinishedAt - job.state.lastRunAt;
|
|
589
918
|
// Floor next-eligible at lastFinishedAt + minRefireGapMs so a
|
|
590
919
|
// misconfigured interval (0/NaN/negative) cannot spin-loop.
|
|
@@ -601,6 +930,58 @@ class Scheduler extends EventEmitter {
|
|
|
601
930
|
})();
|
|
602
931
|
}
|
|
603
932
|
|
|
933
|
+
async _runJob(job, context, workerMeta = {}) {
|
|
934
|
+
const desc = job.descriptor;
|
|
935
|
+
if (!desc.worker || !this._workerPool) {
|
|
936
|
+
return await desc.run(context);
|
|
937
|
+
}
|
|
938
|
+
workerMeta.worker_requested = true;
|
|
939
|
+
const payload = {
|
|
940
|
+
jobName: desc.workerJobName || desc.name,
|
|
941
|
+
trigger: context.trigger,
|
|
942
|
+
modelOverride: context.modelOverride || null,
|
|
943
|
+
};
|
|
944
|
+
// Scheduler watchdogs own job timeouts. The worker request uses the same
|
|
945
|
+
// boundary so a timed-out job retires its occupied worker slot instead of
|
|
946
|
+
// leaving future jobs queued behind an orphaned operation.
|
|
947
|
+
const timeoutMs = desc.workerTimeoutMs ?? desc.timeoutMs ?? this._defaultWorkerJobTimeoutMs ?? 0;
|
|
948
|
+
workerMeta.worker_timeout_ms = timeoutMs || 0;
|
|
949
|
+
try {
|
|
950
|
+
const result = await this._workerPool.request('scheduler.runJob', payload, {
|
|
951
|
+
timeoutMs,
|
|
952
|
+
heavy: true,
|
|
953
|
+
terminateOnTimeout: timeoutMs > 0,
|
|
954
|
+
});
|
|
955
|
+
workerMeta.worker_used = true;
|
|
956
|
+
return result;
|
|
957
|
+
} catch (err) {
|
|
958
|
+
if (err?.code === 'WALL_E_WORKER_REQUEST_TIMEOUT') {
|
|
959
|
+
workerMeta.worker_timeout = true;
|
|
960
|
+
throw err;
|
|
961
|
+
}
|
|
962
|
+
const fallbackLocal = desc.workerFallbackLocal !== false;
|
|
963
|
+
if (!fallbackLocal) throw err;
|
|
964
|
+
workerMeta.worker_fallback = true;
|
|
965
|
+
workerMeta.worker_error = err.code || err.message || 'worker_error';
|
|
966
|
+
console.warn(`[scheduler] Worker job "${desc.name}" failed; falling back to local run: ${err.message}`);
|
|
967
|
+
return await desc.run(context);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
_workerPhaseMetric(result) {
|
|
972
|
+
const phases = Array.isArray(result?._worker_phases) ? result._worker_phases : [];
|
|
973
|
+
if (phases.length === 0) return {};
|
|
974
|
+
let slowest = phases[0];
|
|
975
|
+
for (const phase of phases) {
|
|
976
|
+
if ((phase.duration_ms || 0) > (slowest.duration_ms || 0)) slowest = phase;
|
|
977
|
+
}
|
|
978
|
+
return {
|
|
979
|
+
worker_phase_count: phases.length,
|
|
980
|
+
worker_slowest_phase: String(slowest.name || 'unknown').slice(0, 80),
|
|
981
|
+
worker_slowest_phase_ms: Number(slowest.duration_ms) || 0,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
604
985
|
_checkCascades(completedJobName, result) {
|
|
605
986
|
if (!result || typeof result !== 'object') return;
|
|
606
987
|
|
|
@@ -617,6 +998,22 @@ class Scheduler extends EventEmitter {
|
|
|
617
998
|
_forceReset(name) {
|
|
618
999
|
const job = this._jobs.get(name);
|
|
619
1000
|
if (!job) return;
|
|
1001
|
+
if (this._metricsEnabled()) this._metrics.timeoutCount++;
|
|
1002
|
+
const timeoutErr = new Error('watchdog timeout');
|
|
1003
|
+
timeoutErr.code = 'WALL_E_SCHEDULER_WATCHDOG_TIMEOUT';
|
|
1004
|
+
const run = job._activeRun || {};
|
|
1005
|
+
const durationMs = run.startedAt ? Date.now() - run.startedAt : 0;
|
|
1006
|
+
const metricMeta = {
|
|
1007
|
+
pool: job.descriptor.pool || 'none',
|
|
1008
|
+
trigger: run.trigger || job._wakeTrigger || 'unknown',
|
|
1009
|
+
watchdog_reset: true,
|
|
1010
|
+
...(run.workerMeta || {}),
|
|
1011
|
+
};
|
|
1012
|
+
if (run.op && typeof run.op.end === 'function') {
|
|
1013
|
+
run.op.end({ ok: false, error: timeoutErr, meta: metricMeta });
|
|
1014
|
+
}
|
|
1015
|
+
this._recordJobMetric(name, durationMs, false, timeoutErr, metricMeta);
|
|
1016
|
+
job._activeRun = null;
|
|
620
1017
|
job._epoch++; // invalidate the orphaned promise so its finally block no-ops
|
|
621
1018
|
job.state.status = 'idle';
|
|
622
1019
|
job.state.errorCount++;
|
|
@@ -633,7 +1030,7 @@ class Scheduler extends EventEmitter {
|
|
|
633
1030
|
if (job.descriptor.pool) {
|
|
634
1031
|
this._poolActive.get(job.descriptor.pool)?.delete(name);
|
|
635
1032
|
}
|
|
636
|
-
this._recordJobError(name,
|
|
1033
|
+
this._recordJobError(name, timeoutErr, job.state);
|
|
637
1034
|
this._persistState(name, job);
|
|
638
1035
|
this.emit('jobTimeout', name);
|
|
639
1036
|
}
|
|
@@ -654,11 +1051,17 @@ class Scheduler extends EventEmitter {
|
|
|
654
1051
|
* Returns a summary so the caller can log + telemeter.
|
|
655
1052
|
*/
|
|
656
1053
|
async runMissedJobs(opts = {}) {
|
|
657
|
-
if (!this._persistJobState || !this._persistBrain) {
|
|
658
|
-
return { ran: 0, deferred: 0, reason: 'persistence-disabled' };
|
|
659
|
-
}
|
|
660
1054
|
const max = opts.max ?? this._maxMissedJobsPerRestart;
|
|
661
1055
|
const staggerMs = opts.staggerMs ?? this._missedJobStaggerMs;
|
|
1056
|
+
const op = runtimeHealth.beginOperation('scheduler.runMissedJobs', {
|
|
1057
|
+
max,
|
|
1058
|
+
staggerMs,
|
|
1059
|
+
});
|
|
1060
|
+
if (!this._persistJobState || !this._persistBrain) {
|
|
1061
|
+
const summary = { ran: 0, deferred: 0, reason: 'persistence-disabled' };
|
|
1062
|
+
op.end({ ok: true, meta: summary });
|
|
1063
|
+
return summary;
|
|
1064
|
+
}
|
|
662
1065
|
const skipNames = new Set(opts.skipNames || []);
|
|
663
1066
|
|
|
664
1067
|
let states;
|
|
@@ -666,7 +1069,9 @@ class Scheduler extends EventEmitter {
|
|
|
666
1069
|
states = this._persistBrain.listSchedulerJobStates();
|
|
667
1070
|
} catch (e) {
|
|
668
1071
|
console.warn(`[scheduler] runMissedJobs read failed: ${e.message}`);
|
|
669
|
-
|
|
1072
|
+
const summary = { ran: 0, deferred: 0, reason: 'read-error' };
|
|
1073
|
+
op.end({ ok: false, error: e, meta: summary });
|
|
1074
|
+
return summary;
|
|
670
1075
|
}
|
|
671
1076
|
|
|
672
1077
|
const now = Date.now();
|
|
@@ -689,6 +1094,10 @@ class Scheduler extends EventEmitter {
|
|
|
689
1094
|
skipped.push({ job, reason: 'pool-full' });
|
|
690
1095
|
continue;
|
|
691
1096
|
}
|
|
1097
|
+
if (job.descriptor.worker && this._workerPool && !this._workerPoolHasCapacity()) {
|
|
1098
|
+
this._markWorkerPoolQueued(job, skipped);
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
692
1101
|
candidates.push({ stored, job });
|
|
693
1102
|
}
|
|
694
1103
|
// Sort by oldest-due first (most lag = highest priority)
|
|
@@ -719,6 +1128,8 @@ class Scheduler extends EventEmitter {
|
|
|
719
1128
|
}
|
|
720
1129
|
|
|
721
1130
|
let ran = 0;
|
|
1131
|
+
let workMs = 0;
|
|
1132
|
+
let staggerWaitMs = 0;
|
|
722
1133
|
for (let i = 0; i < immediate.length; i++) {
|
|
723
1134
|
const { job } = immediate[i];
|
|
724
1135
|
// Mark eligible RIGHT NOW so _dispatch picks it up
|
|
@@ -728,18 +1139,22 @@ class Scheduler extends EventEmitter {
|
|
|
728
1139
|
// hooks all apply uniformly. Wait for the run to finish before
|
|
729
1140
|
// staggering the next one.
|
|
730
1141
|
try {
|
|
1142
|
+
const workStartedAt = Date.now();
|
|
731
1143
|
this._dispatch(job);
|
|
732
1144
|
if (job.runPromise) await job.runPromise;
|
|
1145
|
+
workMs += Date.now() - workStartedAt;
|
|
733
1146
|
ran++;
|
|
734
1147
|
} catch (e) {
|
|
735
1148
|
console.warn(`[scheduler] Missed-job replay for "${job.descriptor.name}" threw: ${e.message}`);
|
|
736
1149
|
}
|
|
737
1150
|
if (i < immediate.length - 1 && staggerMs > 0) {
|
|
1151
|
+
const waitStartedAt = Date.now();
|
|
738
1152
|
await new Promise((r) => setTimeout(r, staggerMs));
|
|
1153
|
+
staggerWaitMs += Date.now() - waitStartedAt;
|
|
739
1154
|
}
|
|
740
1155
|
}
|
|
741
1156
|
|
|
742
|
-
|
|
1157
|
+
const summary = {
|
|
743
1158
|
ran,
|
|
744
1159
|
deferred: deferred.length + skipped.length,
|
|
745
1160
|
jobs_ran: immediate.map((c) => c.stored.job_name),
|
|
@@ -748,7 +1163,21 @@ class Scheduler extends EventEmitter {
|
|
|
748
1163
|
...skipped.map((c) => c.job.descriptor.name),
|
|
749
1164
|
],
|
|
750
1165
|
jobs_skipped: skipped.map((c) => ({ name: c.job.descriptor.name, reason: c.reason })),
|
|
1166
|
+
work_ms: workMs,
|
|
1167
|
+
stagger_wait_ms: staggerWaitMs,
|
|
751
1168
|
};
|
|
1169
|
+
runtimeHealth.recordOperation('scheduler.runMissedJobs.work', workMs, {
|
|
1170
|
+
ok: true,
|
|
1171
|
+
meta: { ran, deferred: summary.deferred },
|
|
1172
|
+
});
|
|
1173
|
+
if (staggerWaitMs > 0) {
|
|
1174
|
+
runtimeHealth.recordOperation('scheduler.runMissedJobs.staggerWait', staggerWaitMs, {
|
|
1175
|
+
ok: true,
|
|
1176
|
+
meta: { ran },
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
op.end({ ok: true, meta: { ran: summary.ran, deferred: summary.deferred, work_ms: workMs, stagger_wait_ms: staggerWaitMs } });
|
|
1180
|
+
return summary;
|
|
752
1181
|
}
|
|
753
1182
|
}
|
|
754
1183
|
|