@inetafrica/open-claudia 2.14.8 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +30 -4
- package/CHANGELOG.md +21 -0
- package/README.md +87 -66
- package/bin/agent.js +44 -18
- package/bin/cli.js +30 -28
- package/bin/dream.js +5 -11
- package/bin/loopback-client.js +29 -5
- package/bin/schedule.js +19 -5
- package/bin/tool.js +23 -5
- package/bot-agent.js +5 -2350
- package/bot.js +105 -7
- package/channels/telegram/adapter.js +6 -1
- package/channels/voice/adapter.js +1 -0
- package/channels/voice/manage.js +43 -5
- package/config-dir.js +3 -11
- package/core/actions.js +149 -40
- package/core/approvals.js +136 -29
- package/core/auth-flow.js +103 -19
- package/core/config-dir.js +19 -0
- package/core/config.js +127 -69
- package/core/doctor.js +111 -57
- package/core/dream.js +219 -62
- package/core/enforcer.js +0 -0
- package/core/entities.js +2 -1
- package/core/fsutil.js +55 -0
- package/core/handlers.js +503 -217
- package/core/ideas.js +2 -1
- package/core/identity.js +8 -11
- package/core/io.js +24 -3
- package/core/jobs.js +589 -72
- package/core/lessons.js +3 -2
- package/core/loopback.js +45 -8
- package/core/memory-mutation-queue.js +241 -0
- package/core/migration-backup.js +1060 -0
- package/core/pack-review.js +295 -88
- package/core/packs.js +4 -3
- package/core/persona.js +2 -1
- package/core/process-tree.js +19 -2
- package/core/provider-migration.js +39 -0
- package/core/provider-status.js +80 -0
- package/core/providers/claude-events.js +117 -0
- package/core/providers/claude-hook.js +114 -0
- package/core/providers/claude.js +296 -0
- package/core/providers/codex-events.js +121 -0
- package/core/providers/codex-hook.js +280 -0
- package/core/providers/codex.js +286 -0
- package/core/providers/contract.js +153 -0
- package/core/providers/env.js +148 -0
- package/core/providers/events.js +170 -0
- package/core/providers/hook-command.js +22 -0
- package/core/providers/index.js +240 -0
- package/core/providers/utility-policy.js +269 -0
- package/core/recall/classic.js +2 -2
- package/core/recall/discoverer.js +71 -22
- package/core/recall/metrics.js +27 -1
- package/core/recall/tuning.js +4 -1
- package/core/recall/warm-walker.js +151 -108
- package/core/recall-filter.js +86 -61
- package/core/router.js +79 -7
- package/core/run-context.js +185 -0
- package/core/runner.js +1415 -1282
- package/core/scheduler.js +515 -210
- package/core/side-chat.js +346 -0
- package/core/state.js +1096 -95
- package/core/subagent.js +72 -98
- package/core/system-prompt.js +462 -279
- package/core/tool-guard.js +73 -39
- package/core/tools.js +22 -5
- package/core/transcripts.js +30 -1
- package/core/usage-log.js +19 -4
- package/core/utility-agent.js +654 -0
- package/core/web-auth.js +29 -13
- package/core/web-sessions.js +78 -0
- package/docs/CHANNEL_DESIGN.md +181 -0
- package/docs/MULTI_CHANNEL_PLAN.md +254 -0
- package/docs/PROVIDER_MIGRATION.md +67 -0
- package/health.js +50 -39
- package/package.json +51 -4
- package/setup.js +198 -38
- package/soul.md +39 -0
- package/test-ability-extraction.js +5 -0
- package/test-approval-async.js +63 -4
- package/test-cursor-removal.js +334 -0
- package/test-delivery-contract.js +88 -0
- package/test-enforcer.js +70 -27
- package/test-fixtures/current-provider-parity.json +132 -0
- package/test-fixtures/fake-agent-cli.js +477 -0
- package/test-fixtures/migrations/claude-only/jobs.json +3 -0
- package/test-fixtures/migrations/claude-only/sessions.json +5 -0
- package/test-fixtures/migrations/claude-only/state.json +5 -0
- package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
- package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
- package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
- package/test-fixtures/migrations/cursor-selected/state.json +6 -0
- package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
- package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
- package/test-fixtures/migrations/dual-provider/state.json +10 -0
- package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
- package/test-fixtures/migrations/malformed-partial/state.json +1 -0
- package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
- package/test-fixtures/migrations/missing-project/jobs.json +3 -0
- package/test-fixtures/migrations/missing-project/sessions.json +3 -0
- package/test-fixtures/migrations/missing-project/state.json +4 -0
- package/test-fixtures/migrations/multi-user/jobs.json +4 -0
- package/test-fixtures/migrations/multi-user/sessions.json +4 -0
- package/test-fixtures/migrations/multi-user/state.json +6 -0
- package/test-fixtures/provider-event-samples.json +17 -0
- package/test-learning-e2e.js +5 -0
- package/test-memory-mutation-queue.js +191 -0
- package/test-provider-boot-matrix.js +399 -0
- package/test-provider-bootstrap.js +158 -0
- package/test-provider-capabilities.js +232 -0
- package/test-provider-claude.js +206 -0
- package/test-provider-codex.js +228 -0
- package/test-provider-compaction.js +371 -0
- package/test-provider-core-prompt.js +264 -0
- package/test-provider-coupling-audit.js +90 -0
- package/test-provider-dream.js +312 -0
- package/test-provider-enforcer.js +252 -0
- package/test-provider-env-isolation.js +150 -0
- package/test-provider-events.js +141 -0
- package/test-provider-fixture.js +332 -0
- package/test-provider-gateway.js +508 -0
- package/test-provider-language.js +89 -0
- package/test-provider-migration-backup.js +424 -0
- package/test-provider-pack-review.js +436 -0
- package/test-provider-parity-e2e.js +537 -0
- package/test-provider-prompt-parity.js +89 -0
- package/test-provider-recall.js +271 -0
- package/test-provider-registry.js +251 -0
- package/test-provider-resume-parity.js +41 -0
- package/test-provider-run-lifecycle.js +349 -0
- package/test-provider-runner.js +271 -0
- package/test-provider-scheduler.js +689 -0
- package/test-provider-session-commands.js +185 -0
- package/test-provider-session-history.js +205 -0
- package/test-provider-side-chat.js +337 -0
- package/test-provider-state-migration.js +316 -0
- package/test-provider-stream-decoder.js +69 -0
- package/test-provider-subagent.js +228 -0
- package/test-provider-tool-hooks.js +360 -0
- package/test-recall-discoverer.js +1 -0
- package/test-recall-engine.js +3 -3
- package/test-recall-evolution.js +18 -0
- package/test-run-lock.js +63 -0
- package/test-runner-watchdog-static.js +16 -8
- package/test-single-runtime.js +35 -0
- package/test-telegram-poll-recovery.js +56 -0
- package/test-tool-guard.js +56 -0
- package/test-tools.js +19 -0
- package/test-unified-identity.js +3 -1
- package/test-usage-accounting.js +92 -20
- package/test-utility-provider-policy.js +486 -0
- package/test-web-sessions.js +74 -0
- package/web.js +159 -45
package/core/scheduler.js
CHANGED
|
@@ -1,258 +1,563 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// When a wakeup fires the agent wakes up inside the SAME Claude session
|
|
7
|
-
// it was in when it scheduled the wakeup (per design choice a/i). If
|
|
8
|
-
// the channel is busy with a live turn the fire is deferred 30s and
|
|
9
|
-
// retried. One-shot wakeups self-delete; crons persist.
|
|
1
|
+
// Provider-aware durable scheduler. Every occurrence executes the immutable
|
|
2
|
+
// user/project/provider/settings/session tuple captured when it was created.
|
|
3
|
+
// One-shots are removed only after a terminal successful RunResult and a
|
|
4
|
+
// durable success marker; failures retain the same occurrence ID for bounded
|
|
5
|
+
// retry and eventually become explicit dead letters.
|
|
10
6
|
|
|
11
|
-
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const crypto = require("crypto");
|
|
12
10
|
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
13
12
|
const cron = require("node-cron");
|
|
14
13
|
|
|
15
|
-
const {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
14
|
+
const {
|
|
15
|
+
CRONS_FILE,
|
|
16
|
+
WORKSPACE,
|
|
17
|
+
config,
|
|
18
|
+
} = require("./config");
|
|
19
|
+
const defaultJobsStore = require("./jobs");
|
|
20
|
+
const { runInChat: defaultRunInChat } = require("./context");
|
|
21
|
+
const {
|
|
22
|
+
currentState,
|
|
23
|
+
getActiveProvider,
|
|
24
|
+
getProjectKey,
|
|
25
|
+
getProviderSession,
|
|
26
|
+
getProviderSettings,
|
|
27
|
+
getUserState,
|
|
28
|
+
} = require("./state");
|
|
29
|
+
const { canonicalForChannel } = require("./identity");
|
|
30
|
+
const { send: defaultSend } = require("./io");
|
|
31
|
+
const { providerStatus: defaultProviderStatus } = require("./providers");
|
|
32
|
+
const { redactSensitive } = require("./redact");
|
|
21
33
|
|
|
22
34
|
const MISSED_WAKEUP_GRACE_MS = 60 * 60 * 1000;
|
|
23
|
-
const
|
|
35
|
+
const RETRY_DELAY_MS = 30 * 1000;
|
|
36
|
+
const BUSY_DELAY_MS = 30 * 1000;
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
adapters = Array.isArray(list) ? list.slice() : [];
|
|
38
|
+
function uniqueId(prefix = "occ") {
|
|
39
|
+
return typeof crypto.randomUUID === "function"
|
|
40
|
+
? `${prefix}-${crypto.randomUUID()}`
|
|
41
|
+
: `${prefix}-${Date.now()}-${crypto.randomBytes(6).toString("hex")}`;
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
function
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
if (byId) return byId;
|
|
36
|
-
}
|
|
37
|
-
if (job.adapterType) {
|
|
38
|
-
const byType = adapters.find((a) => a.type === job.adapterType);
|
|
39
|
-
if (byType) return byType;
|
|
40
|
-
}
|
|
41
|
-
return adapters[0] || null;
|
|
44
|
+
function parseFallbacks(value) {
|
|
45
|
+
if (Array.isArray(value)) return [...new Set(value.map((item) => String(item).trim().toLowerCase()).filter(Boolean))];
|
|
46
|
+
return [...new Set(String(value || "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean))];
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
function
|
|
45
|
-
if (!
|
|
46
|
-
if (
|
|
47
|
-
if (!
|
|
48
|
-
return
|
|
49
|
+
function terminalRunSucceeded(result) {
|
|
50
|
+
if (!result || result.ok !== true || (result.status && result.status !== "succeeded")) return false;
|
|
51
|
+
if (result.persistence && Object.values(result.persistence).some((stage) => stage && stage.ok === false)) return false;
|
|
52
|
+
if (!result.delivery || result.delivery.ok !== true) return false;
|
|
53
|
+
return true;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
function
|
|
52
|
-
if (
|
|
53
|
-
if (
|
|
54
|
-
|
|
56
|
+
function providerIsRunnable(status) {
|
|
57
|
+
if (!status || status.availability?.state !== "available") return false;
|
|
58
|
+
if (status.compatibility?.state === "incompatible") return false;
|
|
59
|
+
if (status.auth?.state === "unauthenticated") return false;
|
|
60
|
+
return true;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
function stableFailure(errorOrResult) {
|
|
64
|
+
const error = errorOrResult?.error || errorOrResult;
|
|
65
|
+
const code = String(error?.code || errorOrResult?.status || "SCHEDULED_RUN_FAILED").slice(0, 80);
|
|
66
|
+
const message = redactSensitive(String(
|
|
67
|
+
error?.message
|
|
68
|
+
|| errorOrResult?.diagnostic
|
|
69
|
+
|| (Number.isFinite(errorOrResult?.exitCode) ? `Provider exited with code ${errorOrResult.exitCode}` : "Scheduled provider run failed"),
|
|
70
|
+
)).slice(0, 500);
|
|
71
|
+
return { code, message };
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
function createSchedulerService(dependencies = {}) {
|
|
75
|
+
const jobsStore = dependencies.jobsStore || defaultJobsStore;
|
|
76
|
+
const workspace = dependencies.workspace === undefined ? WORKSPACE : dependencies.workspace;
|
|
77
|
+
const now = dependencies.now || Date.now;
|
|
78
|
+
const setTimer = dependencies.setTimer || setTimeout;
|
|
79
|
+
const clearTimer = dependencies.clearTimer || clearTimeout;
|
|
80
|
+
const scheduleCron = dependencies.scheduleCron || ((expression, callback) => cron.schedule(expression, callback, { noOverlap: true }));
|
|
81
|
+
const validateCron = dependencies.validateCron || cron.validate;
|
|
82
|
+
const runInChat = dependencies.runInChat || defaultRunInChat;
|
|
83
|
+
const stateForUser = dependencies.getUserState || getUserState;
|
|
84
|
+
const send = dependencies.send || defaultSend;
|
|
85
|
+
const providerStatus = dependencies.providerStatus || defaultProviderStatus;
|
|
86
|
+
const fallbackProviders = parseFallbacks(
|
|
87
|
+
dependencies.fallbackProviders === undefined ? config.PROVIDER_FALLBACKS : dependencies.fallbackProviders,
|
|
88
|
+
);
|
|
89
|
+
const retryDelayMs = Number.isFinite(dependencies.retryDelayMs) ? dependencies.retryDelayMs : RETRY_DELAY_MS;
|
|
90
|
+
const busyDelayMs = Number.isFinite(dependencies.busyDelayMs) ? dependencies.busyDelayMs : BUSY_DELAY_MS;
|
|
91
|
+
const missedGraceMs = Number.isFinite(dependencies.missedGraceMs) ? dependencies.missedGraceMs : MISSED_WAKEUP_GRACE_MS;
|
|
92
|
+
const occurrenceIdFactory = dependencies.occurrenceIdFactory || (() => uniqueId("occ"));
|
|
93
|
+
const legacyCronsFile = dependencies.legacyCronsFile === undefined
|
|
94
|
+
? (dependencies.jobsStore ? null : CRONS_FILE)
|
|
95
|
+
: dependencies.legacyCronsFile;
|
|
96
|
+
const runInProvider = dependencies.runAgent || ((...args) => require("./runner").runAgent(...args));
|
|
97
|
+
const admit = dependencies.admitRunContext || ((...args) => require("./runner").admitRunContext(...args));
|
|
98
|
+
const timers = new Map();
|
|
99
|
+
let adapters = Array.isArray(dependencies.adapters) ? dependencies.adapters.slice() : [];
|
|
100
|
+
|
|
101
|
+
function setAdapters(list) {
|
|
102
|
+
adapters = Array.isArray(list) ? list.slice() : [];
|
|
68
103
|
}
|
|
69
|
-
const canonicalUserId = deriveCanonicalUserId(adapter, job);
|
|
70
|
-
const userId = deriveUserId(adapter, job);
|
|
71
|
-
|
|
72
|
-
const ctx = {
|
|
73
|
-
adapter,
|
|
74
|
-
channelId: String(job.channelId),
|
|
75
|
-
canonicalUserId,
|
|
76
|
-
userId,
|
|
77
|
-
transport: adapter.type,
|
|
78
|
-
raw: null,
|
|
79
|
-
};
|
|
80
104
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
console.warn(`scheduler: ${job.id} still busy after retries, skipping`);
|
|
92
|
-
jobs.update(job.id, { lastFireAt: Date.now(), lastFireOk: false });
|
|
93
|
-
return;
|
|
105
|
+
function resolveAdapter(job) {
|
|
106
|
+
if (job.adapter) {
|
|
107
|
+
const exact = adapters.find((adapter) => adapter.id === job.adapter);
|
|
108
|
+
if (exact) return exact;
|
|
109
|
+
}
|
|
110
|
+
if (job.adapterType) {
|
|
111
|
+
const byType = adapters.find((adapter) => adapter.type === job.adapterType);
|
|
112
|
+
if (byType) return byType;
|
|
94
113
|
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function projectDescriptor(job) {
|
|
118
|
+
const explicitDir = job.projectDir && path.isAbsolute(job.projectDir) ? job.projectDir : null;
|
|
119
|
+
const legacyAbsolute = job.project && path.isAbsolute(job.project) ? job.project : null;
|
|
120
|
+
const name = legacyAbsolute ? path.basename(legacyAbsolute) : String(job.project || "");
|
|
121
|
+
const dir = explicitDir || legacyAbsolute || (workspace && name ? path.join(workspace, name) : null);
|
|
122
|
+
return { name, dir: dir || workspace || process.cwd() };
|
|
123
|
+
}
|
|
95
124
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// frozen sessionId when there's no live pointer (cold start) — otherwise
|
|
111
|
-
// a stale, compacted-away id would yank the channel onto a dead thread.
|
|
112
|
-
if (!state.lastSessionId && job.sessionId) runOpts.resumeSessionId = job.sessionId;
|
|
113
|
-
try {
|
|
114
|
-
await runClaude(wrappedPrompt, cwd, null, runOpts);
|
|
115
|
-
jobs.update(job.id, { lastFireAt: Date.now(), lastFireOk: true });
|
|
116
|
-
} catch (e) {
|
|
117
|
-
console.error(`scheduler: ${job.id} run error:`, e.message);
|
|
118
|
-
jobs.update(job.id, { lastFireAt: Date.now(), lastFireOk: false });
|
|
119
|
-
try { await send(`Wakeup "${label}" failed: ${e.message}`); } catch (_) {}
|
|
125
|
+
function resolveCompactionLineage(state, job) {
|
|
126
|
+
let sessionId = job.sessionId || null;
|
|
127
|
+
if (!sessionId) return { sessionId: null, attachSession: true };
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
while (sessionId && !seen.has(sessionId)) {
|
|
130
|
+
seen.add(sessionId);
|
|
131
|
+
const successor = Object.values(state?.completedCompactions || {}).find((entry) => (
|
|
132
|
+
entry?.provider === job.provider
|
|
133
|
+
&& entry?.project === job.project
|
|
134
|
+
&& entry?.previousSessionId === sessionId
|
|
135
|
+
&& entry?.newSessionId
|
|
136
|
+
));
|
|
137
|
+
if (!successor) break;
|
|
138
|
+
sessionId = successor.newSessionId;
|
|
120
139
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
jobs.remove(job.id);
|
|
129
|
-
timers.delete(job.id);
|
|
140
|
+
const live = getProviderSession(state, job.provider, job.project);
|
|
141
|
+
return {
|
|
142
|
+
sessionId,
|
|
143
|
+
// A historical scheduled branch may run, but it must not replace an
|
|
144
|
+
// unrelated live branch. A proven compaction successor can advance it.
|
|
145
|
+
attachSession: !live || live === sessionId,
|
|
146
|
+
};
|
|
130
147
|
}
|
|
131
|
-
}
|
|
132
148
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
149
|
+
function providerStatusSafe(provider) {
|
|
150
|
+
try { return providerStatus(provider); }
|
|
151
|
+
catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
availability: { state: "missing", reason: error.message },
|
|
154
|
+
compatibility: { state: "not_checked" },
|
|
155
|
+
auth: { state: "not_checked" },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
139
158
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
159
|
+
|
|
160
|
+
function executionTarget(job, state) {
|
|
161
|
+
if (providerIsRunnable(providerStatusSafe(job.provider))) {
|
|
162
|
+
const lineage = resolveCompactionLineage(state, job);
|
|
163
|
+
return {
|
|
164
|
+
provider: job.provider,
|
|
165
|
+
providerSettings: job.providerSettings || {},
|
|
166
|
+
sessionId: lineage.sessionId,
|
|
167
|
+
attachSession: lineage.attachSession,
|
|
168
|
+
fallbackFrom: null,
|
|
169
|
+
fresh: !lineage.sessionId,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
for (const fallback of fallbackProviders) {
|
|
173
|
+
if (fallback === job.provider || !["claude", "codex"].includes(fallback)) continue;
|
|
174
|
+
if (!providerIsRunnable(providerStatusSafe(fallback))) continue;
|
|
175
|
+
return {
|
|
176
|
+
provider: fallback,
|
|
177
|
+
providerSettings: {},
|
|
178
|
+
sessionId: null,
|
|
179
|
+
attachSession: false,
|
|
180
|
+
fallbackFrom: job.provider,
|
|
181
|
+
fresh: true,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function wrappedPrompt(job, target) {
|
|
188
|
+
const firedAt = new Date(now()).toISOString();
|
|
189
|
+
const header = job.kind === "cron" ? "Scheduled cron" : "Scheduled wakeup";
|
|
190
|
+
if (target.fallbackFrom) {
|
|
191
|
+
return [
|
|
192
|
+
`[${header} "${job.label}" fired at ${firedAt}; occurrence ${job.occurrenceId}]`,
|
|
193
|
+
`[Archived provider-neutral brief: the originating ${target.fallbackFrom} provider is unavailable. Its native session is not being resumed; this explicitly configured fallback starts fresh.]`,
|
|
194
|
+
job.prompt,
|
|
195
|
+
].join("\n");
|
|
196
|
+
}
|
|
197
|
+
return `[${header} "${job.label}" fired at ${firedAt}; occurrence ${job.occurrenceId}]\n${job.prompt}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function unscheduleJob(id) {
|
|
201
|
+
const timer = timers.get(id);
|
|
202
|
+
if (!timer) return;
|
|
203
|
+
try { timer.stop?.(); } catch (_) {}
|
|
204
|
+
try { if (timer.timeout) clearTimer(timer.timeout); } catch (_) {}
|
|
205
|
+
timers.delete(id);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function armRetry(job, delay = null) {
|
|
209
|
+
unscheduleJob(job.id);
|
|
210
|
+
if (job.disabled || job.status === "dead-letter" || job.status === "succeeded") return;
|
|
211
|
+
const wait = delay === null
|
|
212
|
+
? Math.max(0, Number(job.nextAttemptAt || now()) - now())
|
|
213
|
+
: Math.max(0, delay);
|
|
214
|
+
const timeout = setTimer(() => fireJob(job.id, { reason: "retry" }), wait);
|
|
143
215
|
timers.set(job.id, { timeout });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function markFailure(job, failure, options = {}) {
|
|
219
|
+
const latest = jobsStore.get(job.id) || job;
|
|
220
|
+
const attemptCount = options.attemptAlreadyCounted
|
|
221
|
+
? Math.max(1, latest.attemptCount || 0)
|
|
222
|
+
: (latest.attemptCount || 0) + 1;
|
|
223
|
+
const dead = attemptCount >= (latest.maxAttempts || 1);
|
|
224
|
+
const patch = {
|
|
225
|
+
status: dead ? "dead-letter" : "failed",
|
|
226
|
+
disabled: dead,
|
|
227
|
+
attemptCount,
|
|
228
|
+
nextAttemptAt: dead ? null : now() + retryDelayMs,
|
|
229
|
+
lastFireAt: now(),
|
|
230
|
+
lastFireOk: false,
|
|
231
|
+
lastError: stableFailure(failure),
|
|
232
|
+
};
|
|
233
|
+
const updated = jobsStore.update(job.id, patch) || { ...latest, ...patch };
|
|
234
|
+
if (!dead) armRetry(updated);
|
|
235
|
+
else unscheduleJob(job.id);
|
|
236
|
+
return { ok: false, status: updated.status, job: updated, error: updated.lastError };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function markBusy(job) {
|
|
240
|
+
const latest = jobsStore.get(job.id) || job;
|
|
241
|
+
const busyDeferralCount = (latest.busyDeferralCount || 0) + 1;
|
|
242
|
+
const dead = busyDeferralCount >= (latest.maxAttempts || 1);
|
|
243
|
+
const patch = {
|
|
244
|
+
status: dead ? "dead-letter" : "deferred",
|
|
245
|
+
disabled: dead,
|
|
246
|
+
busyDeferralCount,
|
|
247
|
+
nextAttemptAt: dead ? null : now() + busyDelayMs,
|
|
248
|
+
lastError: dead ? { code: "SCHEDULED_BUSY_EXHAUSTED", message: "Channel remained busy through every bounded deferral" } : null,
|
|
249
|
+
};
|
|
250
|
+
const updated = jobsStore.update(job.id, patch) || { ...latest, ...patch };
|
|
251
|
+
if (!dead) armRetry(updated, busyDelayMs);
|
|
252
|
+
else unscheduleJob(job.id);
|
|
253
|
+
return { ok: false, deferred: !dead, status: updated.status, job: updated };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function settleSuccess(job, result, target) {
|
|
257
|
+
const successPatch = {
|
|
258
|
+
status: "succeeded",
|
|
259
|
+
disabled: false,
|
|
260
|
+
nextAttemptAt: null,
|
|
261
|
+
lastFireAt: now(),
|
|
262
|
+
lastFireOk: true,
|
|
263
|
+
lastError: null,
|
|
264
|
+
completedOccurrenceId: job.occurrenceId,
|
|
265
|
+
lastProvider: target.provider,
|
|
266
|
+
fallbackFromProvider: target.fallbackFrom,
|
|
267
|
+
};
|
|
268
|
+
let marked;
|
|
269
|
+
try { marked = jobsStore.update(job.id, successPatch); }
|
|
270
|
+
catch (error) { return { ok: false, status: "failed", error: stableFailure(error), result }; }
|
|
271
|
+
if (!marked) return { ok: false, status: "failed", error: { code: "JOB_PERSIST_FAILED", message: "Scheduled success marker was not persisted" }, result };
|
|
272
|
+
|
|
273
|
+
if (job.kind === "wakeup") {
|
|
274
|
+
try {
|
|
275
|
+
const removed = jobsStore.remove(job.id);
|
|
276
|
+
if (!removed) throw new Error("successful one-shot could not be removed after its durable success marker");
|
|
277
|
+
unscheduleJob(job.id);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// The succeeded marker prevents a restart from executing this
|
|
280
|
+
// occurrence twice; init() will retry only the final removal.
|
|
281
|
+
return { ok: false, status: "cleanup-pending", error: stableFailure(error), result };
|
|
282
|
+
}
|
|
283
|
+
return { ok: true, status: "succeeded", result };
|
|
148
284
|
}
|
|
149
|
-
|
|
150
|
-
|
|
285
|
+
|
|
286
|
+
const nextOccurrence = occurrenceIdFactory(job);
|
|
287
|
+
const reset = jobsStore.update(job.id, {
|
|
288
|
+
status: "pending",
|
|
289
|
+
disabled: false,
|
|
290
|
+
occurrenceId: nextOccurrence,
|
|
291
|
+
attemptCount: 0,
|
|
292
|
+
busyDeferralCount: 0,
|
|
293
|
+
nextAttemptAt: null,
|
|
294
|
+
});
|
|
295
|
+
if (reset && timers.get(job.id)?.timeout) scheduleJob(reset);
|
|
296
|
+
return reset
|
|
297
|
+
? { ok: true, status: "succeeded", result, job: reset }
|
|
298
|
+
: { ok: false, status: "failed", error: { code: "JOB_PERSIST_FAILED", message: "Recurring job occurrence was not advanced" }, result };
|
|
151
299
|
}
|
|
152
|
-
}
|
|
153
300
|
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
301
|
+
async function fireJob(idOrJob, options = {}) {
|
|
302
|
+
const requested = typeof idOrJob === "string" ? jobsStore.get(idOrJob) : idOrJob;
|
|
303
|
+
const job = requested?.id && jobsStore.get(requested.id) ? jobsStore.get(requested.id) : requested;
|
|
304
|
+
if (!job) return { ok: false, status: "missing" };
|
|
305
|
+
if (job.disabled || job.status === "dead-letter" || job.status === "archived") {
|
|
306
|
+
return { ok: false, status: job.status, skipped: true };
|
|
307
|
+
}
|
|
308
|
+
if (job.status === "running" && Number(job.nextAttemptAt || 0) > now()) {
|
|
309
|
+
return { ok: false, status: "running", skipped: true, inFlight: true };
|
|
310
|
+
}
|
|
311
|
+
if (job.status === "succeeded" && job.kind === "wakeup") {
|
|
312
|
+
try { jobsStore.remove(job.id); } catch (_) {}
|
|
313
|
+
unscheduleJob(job.id);
|
|
314
|
+
return { ok: true, status: "succeeded", finalized: true };
|
|
315
|
+
}
|
|
161
316
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (!Array.isArray(raw) || raw.length === 0) return 0;
|
|
167
|
-
const existing = new Set(jobs.listAll().map((j) => j.id));
|
|
168
|
-
const tg = adapters.find((a) => a.type === "telegram");
|
|
169
|
-
const adapterId = tg ? tg.id : (adapters[0] && adapters[0].id);
|
|
170
|
-
const adapterType = tg ? "telegram" : (adapters[0] && adapters[0].type);
|
|
171
|
-
const channelId = tg ? (CHAT_ID || "") : "";
|
|
172
|
-
const canonicalUserId = adapterType === "telegram"
|
|
173
|
-
? canonicalForTelegram(CHAT_ID || "")
|
|
174
|
-
: canonicalForChannel(adapterType || "", channelId);
|
|
175
|
-
let migrated = 0;
|
|
176
|
-
for (const c of raw) {
|
|
177
|
-
if (!c || !c.schedule || !c.prompt) continue;
|
|
178
|
-
if (c.id && existing.has(c.id)) continue;
|
|
179
|
-
jobs.add({
|
|
180
|
-
id: c.id || jobs.nextId("cron"),
|
|
181
|
-
kind: "cron",
|
|
182
|
-
adapter: adapterId,
|
|
183
|
-
adapterType,
|
|
184
|
-
channelId: String(channelId),
|
|
185
|
-
canonicalUserId,
|
|
186
|
-
project: c.project || null,
|
|
187
|
-
prompt: c.prompt,
|
|
188
|
-
label: c.label || (c.prompt || "").slice(0, 50),
|
|
189
|
-
source: "user",
|
|
190
|
-
schedule: c.schedule,
|
|
191
|
-
sessionKey: null,
|
|
192
|
-
sessionId: null,
|
|
193
|
-
});
|
|
194
|
-
migrated++;
|
|
317
|
+
const adapter = resolveAdapter(job);
|
|
318
|
+
if (!adapter) {
|
|
319
|
+
try { return markFailure(job, { code: "SCHEDULED_ADAPTER_UNAVAILABLE", message: "Saved channel adapter is unavailable" }); }
|
|
320
|
+
catch (error) { return { ok: false, status: "failed", error: stableFailure(error) }; }
|
|
195
321
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
322
|
+
const canonicalUserId = job.canonicalUserId || canonicalForChannel(adapter.type, job.channelId);
|
|
323
|
+
const userId = String(job.userId || (adapter.type === "telegram" ? job.channelId : canonicalUserId || job.channelId || ""));
|
|
324
|
+
const chat = {
|
|
325
|
+
adapter,
|
|
326
|
+
channelId: String(job.channelId),
|
|
327
|
+
canonicalUserId,
|
|
328
|
+
userId,
|
|
329
|
+
transport: adapter.type,
|
|
330
|
+
raw: null,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
return runInChat(chat, async () => {
|
|
334
|
+
const state = stateForUser(canonicalUserId);
|
|
335
|
+
if (state.runningProcess || state.preparingRun || state.isCompacting) {
|
|
336
|
+
try { return markBusy(job); }
|
|
337
|
+
catch (error) { return { ok: false, status: "failed", error: stableFailure(error) }; }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const target = executionTarget(job, state);
|
|
341
|
+
if (!target) {
|
|
342
|
+
try {
|
|
343
|
+
return markFailure(job, {
|
|
344
|
+
code: "SCHEDULED_PROVIDER_UNAVAILABLE",
|
|
345
|
+
message: `Saved provider ${job.provider} is unavailable and no explicit fallback is configured`,
|
|
346
|
+
});
|
|
347
|
+
} catch (error) { return { ok: false, status: "failed", error: stableFailure(error) }; }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const project = projectDescriptor(job);
|
|
351
|
+
const prompt = wrappedPrompt(job, target);
|
|
352
|
+
const runOptions = {
|
|
353
|
+
purpose: "scheduled",
|
|
354
|
+
provider: target.provider,
|
|
355
|
+
providerSettings: target.providerSettings,
|
|
356
|
+
project,
|
|
357
|
+
fresh: target.fresh,
|
|
358
|
+
resumeSessionId: target.sessionId || null,
|
|
359
|
+
attachSession: target.attachSession,
|
|
360
|
+
requiredDelivery: true,
|
|
361
|
+
occurrenceId: job.occurrenceId,
|
|
362
|
+
scheduledJobId: job.id,
|
|
363
|
+
originRunId: job.originRunId || null,
|
|
364
|
+
};
|
|
365
|
+
// Admission is synchronous and precedes all notification delivery.
|
|
366
|
+
const runContext = admit(prompt, project.dir, null, runOptions);
|
|
367
|
+
|
|
368
|
+
let running;
|
|
369
|
+
try {
|
|
370
|
+
running = jobsStore.update(job.id, {
|
|
371
|
+
status: "running",
|
|
372
|
+
disabled: false,
|
|
373
|
+
attemptCount: (job.attemptCount || 0) + 1,
|
|
374
|
+
lastAttemptAt: now(),
|
|
375
|
+
nextAttemptAt: now() + retryDelayMs,
|
|
376
|
+
lastProvider: target.provider,
|
|
377
|
+
fallbackFromProvider: target.fallbackFrom,
|
|
378
|
+
});
|
|
379
|
+
if (!running) throw new Error("scheduled attempt could not be persisted");
|
|
380
|
+
} catch (error) {
|
|
381
|
+
return { ok: false, status: "failed", error: stableFailure(error) };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try { await send(`Wakeup: ${job.label}`); } catch (_) { /* advisory only */ }
|
|
385
|
+
let result;
|
|
386
|
+
try { result = await runInProvider(prompt, runContext.cwd, null, { ...runOptions, runContext }); }
|
|
387
|
+
catch (error) {
|
|
388
|
+
try { return markFailure(running, error, { attemptAlreadyCounted: true }); }
|
|
389
|
+
catch (persistError) { return { ok: false, status: "failed", error: stableFailure(persistError) }; }
|
|
390
|
+
}
|
|
391
|
+
if (!terminalRunSucceeded(result)) {
|
|
392
|
+
try { return markFailure(running, result, { attemptAlreadyCounted: true }); }
|
|
393
|
+
catch (error) { return { ok: false, status: "failed", error: stableFailure(error), result }; }
|
|
394
|
+
}
|
|
395
|
+
return settleSuccess(running, result, target);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function scheduleJob(job) {
|
|
400
|
+
unscheduleJob(job.id);
|
|
401
|
+
if (!job || job.disabled || ["archived", "dead-letter"].includes(job.status)) return false;
|
|
402
|
+
if (job.status === "succeeded" && job.kind === "wakeup") {
|
|
403
|
+
try { jobsStore.remove(job.id); } catch (_) {}
|
|
404
|
+
return false;
|
|
200
405
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
406
|
+
if (["failed", "deferred", "running"].includes(job.status)) {
|
|
407
|
+
armRetry(job, Math.max(0, Number(job.nextAttemptAt || now()) - now()));
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
if (job.kind === "wakeup") {
|
|
411
|
+
const fireAt = Number(job.fireAt || 0);
|
|
412
|
+
const overdue = now() - fireAt;
|
|
413
|
+
if (overdue > missedGraceMs) {
|
|
414
|
+
try {
|
|
415
|
+
jobsStore.update(job.id, {
|
|
416
|
+
status: "dead-letter",
|
|
417
|
+
disabled: true,
|
|
418
|
+
nextAttemptAt: null,
|
|
419
|
+
lastError: { code: "MISSED_WAKEUP_EXPIRED", message: "Wakeup exceeded the missed-fire grace window" },
|
|
420
|
+
});
|
|
421
|
+
} catch (_) {}
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
const timeout = setTimer(() => fireJob(job.id, { reason: fireAt < now() ? "missed" : "timer" }), Math.max(0, fireAt - now()));
|
|
425
|
+
timers.set(job.id, { timeout });
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
if (job.kind === "cron") {
|
|
429
|
+
if (!validateCron(job.schedule)) return false;
|
|
430
|
+
const task = scheduleCron(job.schedule, () => {
|
|
431
|
+
const current = jobsStore.get(job.id);
|
|
432
|
+
if (!current || current.disabled || current.status !== "pending") return;
|
|
433
|
+
fireJob(current.id, { reason: "cron" });
|
|
434
|
+
});
|
|
435
|
+
timers.set(job.id, { stop: () => task.stop?.() });
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
205
439
|
}
|
|
206
|
-
}
|
|
207
440
|
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
441
|
+
function archiveLegacyCrons() {
|
|
442
|
+
if (!legacyCronsFile || !fs.existsSync(legacyCronsFile)) return 0;
|
|
443
|
+
let raw;
|
|
444
|
+
try { raw = JSON.parse(fs.readFileSync(legacyCronsFile, "utf8")); }
|
|
445
|
+
catch (_) { return 0; }
|
|
446
|
+
if (!Array.isArray(raw)) return 0;
|
|
447
|
+
const existing = new Set([
|
|
448
|
+
...jobsStore.listAll().map((job) => job.id),
|
|
449
|
+
...(jobsStore.listArchived ? jobsStore.listArchived().map((job) => job.id) : []),
|
|
450
|
+
]);
|
|
451
|
+
let count = 0;
|
|
452
|
+
for (const legacy of raw) {
|
|
453
|
+
if (!legacy || !legacy.schedule || !legacy.prompt) continue;
|
|
454
|
+
const id = legacy.id || `legacy-cron-${crypto.createHash("sha256").update(JSON.stringify(legacy)).digest("hex").slice(0, 16)}`;
|
|
455
|
+
if (existing.has(id)) continue;
|
|
456
|
+
jobsStore.archive({ ...legacy, id, kind: "cron" }, "Legacy cron had no saved provider/session identity and is disabled");
|
|
457
|
+
existing.add(id);
|
|
458
|
+
count += 1;
|
|
217
459
|
}
|
|
218
|
-
|
|
460
|
+
return count;
|
|
219
461
|
}
|
|
220
|
-
}
|
|
221
462
|
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
catch (e) { console.error(`scheduler: failed to schedule ${job.id}:`, e.message); }
|
|
463
|
+
function init() {
|
|
464
|
+
jobsStore.ensureMigrated?.();
|
|
465
|
+
archiveLegacyCrons();
|
|
466
|
+
for (const job of jobsStore.listAll()) scheduleJob(job);
|
|
467
|
+
return jobsStore.listAll().length;
|
|
228
468
|
}
|
|
229
|
-
fireMissedWakeupsOnce();
|
|
230
|
-
console.log(`scheduler: loaded ${jobs.listAll().length} job(s)`);
|
|
231
|
-
}
|
|
232
469
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
470
|
+
function enrichJobSpec(spec) {
|
|
471
|
+
const state = dependencies.currentState ? dependencies.currentState() : currentState();
|
|
472
|
+
const provider = spec.provider || getActiveProvider(state);
|
|
473
|
+
const project = spec.project || getProjectKey(state);
|
|
474
|
+
const projectDir = spec.projectDir || (state.currentSession?.name === project ? state.currentSession?.dir : null);
|
|
475
|
+
const sessionId = Object.prototype.hasOwnProperty.call(spec, "sessionId")
|
|
476
|
+
? spec.sessionId
|
|
477
|
+
: getProviderSession(state, provider, project);
|
|
478
|
+
const originRunId = spec.originRunId || state.activeRunContext?.runId || null;
|
|
479
|
+
const providerSettings = spec.providerSettings || getProviderSettings(state, provider);
|
|
480
|
+
return {
|
|
481
|
+
...spec,
|
|
482
|
+
id: spec.id || defaultJobsStore.nextId(spec.kind || "job"),
|
|
483
|
+
adapter: spec.adapter || dependencies.defaultAdapter || null,
|
|
484
|
+
adapterType: spec.adapterType || dependencies.defaultAdapterType || null,
|
|
485
|
+
channelId: spec.channelId == null ? null : String(spec.channelId),
|
|
486
|
+
canonicalUserId: spec.canonicalUserId || state.userId || null,
|
|
487
|
+
userId: spec.userId || null,
|
|
488
|
+
provider,
|
|
489
|
+
providerSettings: structuredClone(providerSettings || {}),
|
|
490
|
+
project,
|
|
491
|
+
projectDir,
|
|
492
|
+
sessionId: sessionId || null,
|
|
493
|
+
previousSessionId: spec.previousSessionId || sessionId || null,
|
|
494
|
+
originRunId,
|
|
495
|
+
sessionLineage: {
|
|
496
|
+
...(spec.sessionLineage || {}),
|
|
497
|
+
originRunId,
|
|
498
|
+
originSessionId: spec.sessionLineage?.originSessionId || sessionId || null,
|
|
499
|
+
},
|
|
500
|
+
occurrenceId: spec.occurrenceId || occurrenceIdFactory(spec),
|
|
501
|
+
status: "pending",
|
|
502
|
+
disabled: false,
|
|
503
|
+
attemptCount: 0,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
239
506
|
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
|
|
507
|
+
function addJob(spec) {
|
|
508
|
+
const job = jobsStore.add(enrichJobSpec(spec));
|
|
509
|
+
scheduleJob(job);
|
|
510
|
+
return job;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function removeJob(id) {
|
|
514
|
+
unscheduleJob(id);
|
|
515
|
+
return jobsStore.remove(id);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function stopAll() {
|
|
519
|
+
for (const id of [...timers.keys()]) unscheduleJob(id);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
setAdapters,
|
|
524
|
+
init,
|
|
525
|
+
scheduleJob,
|
|
526
|
+
unscheduleJob,
|
|
527
|
+
addJob,
|
|
528
|
+
removeJob,
|
|
529
|
+
stopAll,
|
|
530
|
+
fireJob,
|
|
531
|
+
archiveLegacyCrons,
|
|
532
|
+
enrichJobSpec,
|
|
533
|
+
resolveCompactionLineage,
|
|
534
|
+
timers,
|
|
535
|
+
};
|
|
243
536
|
}
|
|
244
537
|
|
|
245
|
-
|
|
246
|
-
|
|
538
|
+
const service = createSchedulerService();
|
|
539
|
+
|
|
540
|
+
function setAdapters(list) { return service.setAdapters(list); }
|
|
541
|
+
function initScheduler(adapterList) {
|
|
542
|
+
service.setAdapters(adapterList);
|
|
543
|
+
const count = service.init();
|
|
544
|
+
console.log(`scheduler: loaded ${count} job(s)`);
|
|
545
|
+
return count;
|
|
247
546
|
}
|
|
248
547
|
|
|
249
548
|
module.exports = {
|
|
549
|
+
MISSED_WAKEUP_GRACE_MS,
|
|
550
|
+
RETRY_DELAY_MS,
|
|
551
|
+
BUSY_DELAY_MS,
|
|
552
|
+
createSchedulerService,
|
|
553
|
+
parseFallbacks,
|
|
554
|
+
terminalRunSucceeded,
|
|
250
555
|
setAdapters,
|
|
251
556
|
initScheduler,
|
|
252
|
-
scheduleJob,
|
|
253
|
-
unscheduleJob,
|
|
254
|
-
addJob,
|
|
255
|
-
removeJob,
|
|
256
|
-
stopAll,
|
|
257
|
-
fireJob,
|
|
557
|
+
scheduleJob: service.scheduleJob,
|
|
558
|
+
unscheduleJob: service.unscheduleJob,
|
|
559
|
+
addJob: service.addJob,
|
|
560
|
+
removeJob: service.removeJob,
|
|
561
|
+
stopAll: service.stopAll,
|
|
562
|
+
fireJob: service.fireJob,
|
|
258
563
|
};
|