@cjhyy/code-shell-core 0.5.0-rc.1 → 0.5.0-rc.2
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/dist/agent/agent-definition-registry.d.ts +18 -3
- package/dist/agent/agent-definition-registry.js +47 -18
- package/dist/agent/agent-definition.d.ts +17 -0
- package/dist/agent/agent-definition.js +22 -1
- package/dist/arena/arena.js +11 -12
- package/dist/arena/context/context-tools.js +2 -4
- package/dist/arena/context/within-root.d.ts +7 -0
- package/dist/arena/context/within-root.js +15 -0
- package/dist/arena/iterate/phases/argue.js +8 -2
- package/dist/arena/iterate/phases/revise.js +1 -1
- package/dist/arena/iterate/phases/tournament.js +3 -3
- package/dist/arena/phases/adjudication.js +1 -4
- package/dist/arena/phases/build-consensus.js +1 -4
- package/dist/arena/phases/cross-review.js +2 -8
- package/dist/arena/phases/debate-rounds.js +1 -4
- package/dist/arena/phases/participant-research.js +1 -4
- package/dist/arena/phases/planning-detail-expansion.js +8 -10
- package/dist/arena/planner.js +0 -1
- package/dist/arena/providers/docs.d.ts +1 -0
- package/dist/arena/providers/docs.js +5 -2
- package/dist/arena/providers/repo.d.ts +1 -0
- package/dist/arena/providers/repo.js +5 -2
- package/dist/arena/strategies/utils.js +36 -3
- package/dist/arena/transitions.d.ts +7 -2
- package/dist/arena/transitions.js +9 -4
- package/dist/arena/types.d.ts +8 -2
- package/dist/automation/cron-expr.d.ts +31 -0
- package/dist/automation/cron-expr.js +151 -0
- package/dist/automation/index.d.ts +41 -0
- package/dist/automation/index.js +39 -0
- package/dist/automation/runner.d.ts +67 -0
- package/dist/automation/runner.js +56 -0
- package/dist/automation/scheduler.d.ts +156 -0
- package/dist/automation/scheduler.js +546 -0
- package/dist/automation/store.d.ts +39 -0
- package/dist/automation/store.js +119 -0
- package/dist/automation/write-policy.d.ts +34 -0
- package/dist/automation/write-policy.js +113 -0
- package/dist/automation/write-run.d.ts +51 -0
- package/dist/automation/write-run.js +38 -0
- package/dist/capability-control/index.d.ts +5 -0
- package/dist/capability-control/index.js +3 -0
- package/dist/capability-control/overlay.d.ts +49 -0
- package/dist/capability-control/overlay.js +89 -0
- package/dist/capability-control/project.d.ts +65 -0
- package/dist/capability-control/project.js +147 -0
- package/dist/capability-control/service.d.ts +70 -0
- package/dist/capability-control/service.js +147 -0
- package/dist/capability-control/types.d.ts +62 -0
- package/dist/capability-control/types.js +17 -0
- package/dist/cli/agent-server-stdio.d.ts +12 -1
- package/dist/cli/agent-server-stdio.js +114 -19
- package/dist/cli/agent-server-tcp.d.ts +20 -0
- package/dist/cli/agent-server-tcp.js +120 -0
- package/dist/cli/graceful-shutdown.d.ts +27 -0
- package/dist/cli/graceful-shutdown.js +30 -0
- package/dist/context/compaction.js +23 -15
- package/dist/context/manager.d.ts +0 -20
- package/dist/context/manager.js +0 -37
- package/dist/cron/cron-runtime.d.ts +2 -0
- package/dist/cron/cron-runtime.js +2 -0
- package/dist/cron/cron-store.d.ts +2 -0
- package/dist/cron/cron-store.js +2 -0
- package/dist/cron/scheduler.d.ts +5 -32
- package/dist/cron/scheduler.js +5 -113
- package/dist/data/openrouter-sync.js +1 -1
- package/dist/engine/engine.d.ts +208 -14
- package/dist/engine/engine.js +754 -169
- package/dist/engine/goal.d.ts +42 -0
- package/dist/engine/goal.js +52 -0
- package/dist/engine/image-compression.d.ts +81 -0
- package/dist/engine/image-compression.js +186 -0
- package/dist/engine/image-policy.d.ts +152 -0
- package/dist/engine/image-policy.js +165 -0
- package/dist/engine/reactive-threshold.d.ts +13 -0
- package/dist/engine/reactive-threshold.js +17 -0
- package/dist/engine/runtime.js +7 -0
- package/dist/engine/session-title.d.ts +13 -0
- package/dist/engine/session-title.js +34 -0
- package/dist/engine/turn-loop.d.ts +29 -8
- package/dist/engine/turn-loop.js +498 -316
- package/dist/git/parse-log.d.ts +13 -0
- package/dist/git/parse-log.js +21 -0
- package/dist/git/utils.d.ts +8 -6
- package/dist/git/utils.js +57 -26
- package/dist/git/worktree.js +33 -18
- package/dist/hooks/events.d.ts +23 -1
- package/dist/hooks/goal-stop-hook.d.ts +30 -0
- package/dist/hooks/goal-stop-hook.js +102 -0
- package/dist/hooks/hook-output.d.ts +33 -0
- package/dist/hooks/hook-output.js +79 -0
- package/dist/hooks/registry.d.ts +7 -0
- package/dist/hooks/registry.js +38 -1
- package/dist/hooks/shell-runner.d.ts +2 -0
- package/dist/hooks/shell-runner.js +43 -2
- package/dist/index.d.ts +31 -6
- package/dist/index.js +30 -3
- package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
- package/dist/llm/capabilities/reasoning-control.js +38 -0
- package/dist/llm/capabilities/rules.js +33 -3
- package/dist/llm/capabilities/types.d.ts +22 -2
- package/dist/llm/clamp-max-tokens.d.ts +11 -0
- package/dist/llm/clamp-max-tokens.js +17 -0
- package/dist/llm/client-base.d.ts +41 -4
- package/dist/llm/client-base.js +67 -13
- package/dist/llm/client-factory.d.ts +3 -3
- package/dist/llm/client-factory.js +2 -2
- package/dist/llm/model-pool.d.ts +15 -11
- package/dist/llm/model-pool.js +23 -20
- package/dist/llm/provider-catalog.d.ts +2 -2
- package/dist/llm/providers/anthropic.d.ts +32 -2
- package/dist/llm/providers/anthropic.js +147 -8
- package/dist/llm/providers/openai.d.ts +11 -2
- package/dist/llm/providers/openai.js +253 -52
- package/dist/llm/reasoning-setting.d.ts +51 -0
- package/dist/llm/reasoning-setting.js +32 -0
- package/dist/llm/stop-reason.d.ts +13 -0
- package/dist/llm/stop-reason.js +21 -0
- package/dist/llm/strip-vision.d.ts +32 -0
- package/dist/llm/strip-vision.js +94 -0
- package/dist/llm/types.d.ts +5 -6
- package/dist/logging/logger.js +18 -4
- package/dist/logging/sanitize-messages.d.ts +10 -0
- package/dist/logging/sanitize-messages.js +90 -0
- package/dist/lsp/client.js +28 -8
- package/dist/lsp/manager.js +2 -1
- package/dist/lsp/root-path.d.ts +9 -0
- package/dist/lsp/root-path.js +12 -0
- package/dist/onboarding.js +18 -11
- package/dist/plugins/gitOps.d.ts +34 -0
- package/dist/plugins/gitOps.js +59 -3
- package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
- package/dist/plugins/installer/codex/convertAgents.js +55 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
- package/dist/plugins/installer/codex/convertMcp.js +45 -0
- package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
- package/dist/plugins/installer/codex/convertSkills.js +33 -0
- package/dist/plugins/installer/detectFormat.d.ts +2 -0
- package/dist/plugins/installer/detectFormat.js +6 -0
- package/dist/plugins/installer/install.d.ts +7 -0
- package/dist/plugins/installer/install.js +99 -0
- package/dist/plugins/installer/installFromSource.d.ts +13 -0
- package/dist/plugins/installer/installFromSource.js +45 -0
- package/dist/plugins/installer/list.d.ts +9 -0
- package/dist/plugins/installer/list.js +30 -0
- package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
- package/dist/plugins/installer/loadPluginAgents.js +23 -0
- package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
- package/dist/plugins/installer/loadPluginMcp.js +74 -0
- package/dist/plugins/installer/parseSource.d.ts +20 -0
- package/dist/plugins/installer/parseSource.js +74 -0
- package/dist/plugins/installer/paths.d.ts +5 -0
- package/dist/plugins/installer/paths.js +25 -0
- package/dist/plugins/installer/types.d.ts +49 -0
- package/dist/plugins/installer/types.js +27 -0
- package/dist/plugins/installer/uninstall.d.ts +2 -0
- package/dist/plugins/installer/uninstall.js +14 -0
- package/dist/plugins/installer/update.d.ts +14 -0
- package/dist/plugins/installer/update.js +65 -0
- package/dist/plugins/loadPluginHooks.d.ts +8 -1
- package/dist/plugins/loadPluginHooks.js +11 -1
- package/dist/plugins/marketplaceManager.d.ts +8 -1
- package/dist/plugins/marketplaceManager.js +38 -4
- package/dist/plugins/pluginCommandHook.js +34 -0
- package/dist/plugins/pluginInstaller.d.ts +21 -0
- package/dist/plugins/pluginInstaller.js +128 -29
- package/dist/plugins/schemas.js +40 -6
- package/dist/plugins/types.d.ts +4 -0
- package/dist/preset/index.js +16 -0
- package/dist/prompt/composer.d.ts +4 -0
- package/dist/prompt/composer.js +28 -4
- package/dist/prompt/sections/orchestration.md +8 -0
- package/dist/protocol/chat-session-manager.d.ts +8 -1
- package/dist/protocol/chat-session-manager.js +10 -0
- package/dist/protocol/chat-session.d.ts +17 -0
- package/dist/protocol/chat-session.js +28 -0
- package/dist/protocol/redact.d.ts +50 -0
- package/dist/protocol/redact.js +71 -0
- package/dist/protocol/server.d.ts +41 -2
- package/dist/protocol/server.js +231 -33
- package/dist/protocol/tcp-transport.d.ts +45 -0
- package/dist/protocol/tcp-transport.js +74 -0
- package/dist/protocol/types.d.ts +23 -0
- package/dist/protocol/types.js +6 -0
- package/dist/remote/bridge.d.ts +11 -2
- package/dist/remote/bridge.js +61 -33
- package/dist/run/ArtifactTracker.js +5 -4
- package/dist/run/EngineRunner.d.ts +27 -1
- package/dist/run/EngineRunner.js +46 -8
- package/dist/run/FileRunStore.js +32 -8
- package/dist/run/RunLock.d.ts +28 -2
- package/dist/run/RunLock.js +49 -7
- package/dist/run/RunManager.js +51 -3
- package/dist/run/RunQueue.d.ts +1 -0
- package/dist/run/RunQueue.js +8 -2
- package/dist/run/factory.d.ts +7 -0
- package/dist/run/factory.js +1 -0
- package/dist/run/index.d.ts +2 -2
- package/dist/run/index.js +1 -1
- package/dist/run/redirect-target.d.ts +7 -0
- package/dist/run/redirect-target.js +13 -0
- package/dist/run/types.js +1 -1
- package/dist/services/browser-open.d.ts +13 -0
- package/dist/services/browser-open.js +16 -0
- package/dist/services/dream-consolidation.d.ts +57 -0
- package/dist/services/dream-consolidation.js +151 -0
- package/dist/services/memory-orchestrator.js +18 -0
- package/dist/services/notifier.d.ts +8 -0
- package/dist/services/notifier.js +39 -25
- package/dist/services/oauth.js +5 -7
- package/dist/services/session-memory-sort.d.ts +8 -0
- package/dist/services/session-memory-sort.js +9 -0
- package/dist/services/session-memory.js +8 -9
- package/dist/session/file-history.js +10 -3
- package/dist/session/session-manager.d.ts +23 -1
- package/dist/session/session-manager.js +73 -2
- package/dist/settings/disk-defaults.d.ts +35 -0
- package/dist/settings/disk-defaults.js +24 -0
- package/dist/settings/manager.d.ts +33 -0
- package/dist/settings/manager.js +94 -1
- package/dist/settings/personalization.d.ts +19 -0
- package/dist/settings/personalization.js +7 -0
- package/dist/settings/schema.d.ts +1142 -126
- package/dist/settings/schema.js +119 -10
- package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
- package/dist/tool-system/builtin/add-marketplace.js +76 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +11 -0
- package/dist/tool-system/builtin/agent-registry.js +8 -4
- package/dist/tool-system/builtin/agent.d.ts +29 -10
- package/dist/tool-system/builtin/agent.js +108 -52
- package/dist/tool-system/builtin/apply-patch/index.js +22 -3
- package/dist/tool-system/builtin/arena.js +6 -7
- package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
- package/dist/tool-system/builtin/complete-goal.js +45 -0
- package/dist/tool-system/builtin/cron.js +53 -9
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +6 -1
- package/dist/tool-system/builtin/generate-image.d.ts +25 -0
- package/dist/tool-system/builtin/generate-image.js +146 -0
- package/dist/tool-system/builtin/glob.js +7 -0
- package/dist/tool-system/builtin/grep.js +6 -0
- package/dist/tool-system/builtin/index.d.ts +18 -1
- package/dist/tool-system/builtin/index.js +62 -1
- package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
- package/dist/tool-system/builtin/notebook-edit.js +8 -1
- package/dist/tool-system/builtin/read.d.ts +2 -1
- package/dist/tool-system/builtin/read.js +9 -1
- package/dist/tool-system/builtin/sleep.js +9 -3
- package/dist/tool-system/builtin/task.js +4 -1
- package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
- package/dist/tool-system/builtin/update-automation-memory.js +60 -0
- package/dist/tool-system/builtin/view-image.d.ts +18 -0
- package/dist/tool-system/builtin/view-image.js +96 -0
- package/dist/tool-system/builtin/web-search.d.ts +4 -1
- package/dist/tool-system/builtin/web-search.js +36 -3
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +7 -1
- package/dist/tool-system/context.d.ts +12 -0
- package/dist/tool-system/executor.d.ts +0 -1
- package/dist/tool-system/executor.js +34 -40
- package/dist/tool-system/investigation-guard.d.ts +4 -1
- package/dist/tool-system/investigation-guard.js +19 -3
- package/dist/tool-system/mcp-manager.d.ts +36 -12
- package/dist/tool-system/mcp-manager.js +173 -10
- package/dist/tool-system/path-policy.d.ts +94 -0
- package/dist/tool-system/path-policy.js +279 -0
- package/dist/tool-system/permission.js +43 -50
- package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
- package/dist/tool-system/plan-mode-allowlist.js +43 -0
- package/dist/tool-system/registry.js +12 -1
- package/dist/tool-system/validation.d.ts +10 -0
- package/dist/tool-system/validation.js +16 -3
- package/dist/types.d.ts +90 -15
- package/dist/utils/format.js +4 -2
- package/dist/utils/lockfile.js +9 -2
- package/dist/utils/theme.d.ts +6 -0
- package/dist/utils/theme.js +4 -2
- package/package.json +2 -1
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron scheduler — schedule and manage recurring agent tasks.
|
|
3
|
+
*
|
|
4
|
+
* Supports simple cron-like intervals. Each job runs the Engine
|
|
5
|
+
* with a predefined prompt on schedule.
|
|
6
|
+
*/
|
|
7
|
+
import { isCronExpression, parseCronExpression, nextCronTime } from "./cron-expr.js";
|
|
8
|
+
export class CronScheduler {
|
|
9
|
+
jobs = new Map();
|
|
10
|
+
timers = new Map();
|
|
11
|
+
/** Job ids with an execution currently in flight — prevents a tick from
|
|
12
|
+
* starting a second run of a job whose previous run hasn't finished. */
|
|
13
|
+
running = new Set();
|
|
14
|
+
/** In-flight runs keyed by job id: the AbortController that cancels the run,
|
|
15
|
+
* plus `done` — a promise that resolves when the run has FULLY settled
|
|
16
|
+
* (executor returned, including any post-abort bookkeeping like Engine's
|
|
17
|
+
* final saveState). The re-entrancy guard (`running`) ensures at most one
|
|
18
|
+
* run per job, so one entry per job id is sufficient. `abort(jobId)` trips
|
|
19
|
+
* the controller and returns `done` so callers can wait for the run to
|
|
20
|
+
* actually stop before acting (e.g. deleting its session dir without racing
|
|
21
|
+
* a final write that would recreate it). */
|
|
22
|
+
runningControllers = new Map();
|
|
23
|
+
nextId = 1;
|
|
24
|
+
onExecute;
|
|
25
|
+
/** Optional persistence backend. When set, every create/delete/pause/resume
|
|
26
|
+
* writes the full job set to disk so jobs survive a restart. */
|
|
27
|
+
store;
|
|
28
|
+
/** When false, the scheduler persists + tracks jobs but NEVER arms timers
|
|
29
|
+
* (so it can't execute). Used by the desktop agent worker, a separate
|
|
30
|
+
* process from the main process that owns execution. Default true. */
|
|
31
|
+
executionEnabled = true;
|
|
32
|
+
constructor(store) {
|
|
33
|
+
this.store = store;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Disable (or re-enable) timer arming / execution. A host that only needs to
|
|
37
|
+
* persist + read jobs — never run them — calls setExecutionEnabled(false).
|
|
38
|
+
* Stops any timers already armed when turning off.
|
|
39
|
+
*/
|
|
40
|
+
setExecutionEnabled(enabled) {
|
|
41
|
+
this.executionEnabled = enabled;
|
|
42
|
+
if (!enabled) {
|
|
43
|
+
for (const timer of this.timers.values())
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
this.timers.clear();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Attach (or replace) the persistence backend. Used to give the shared
|
|
49
|
+
* singleton a store at runtime startup without changing its identity. */
|
|
50
|
+
setStore(store) {
|
|
51
|
+
this.store = store;
|
|
52
|
+
}
|
|
53
|
+
setExecutor(fn) {
|
|
54
|
+
this.onExecute = fn;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Abort the in-flight run of `jobId`, if any. Aborts the AbortSignal handed
|
|
58
|
+
* to the executor (which Engine.run cooperates with — it resolves with
|
|
59
|
+
* reason "aborted_streaming" rather than throwing). Returns false when the
|
|
60
|
+
* job has no run currently in flight (already settled, never started, or
|
|
61
|
+
* unknown id). The run's own finally block clears the controller, so a
|
|
62
|
+
* settled run is a no-op here.
|
|
63
|
+
*/
|
|
64
|
+
async abort(jobId) {
|
|
65
|
+
const entry = this.runningControllers.get(jobId);
|
|
66
|
+
if (!entry)
|
|
67
|
+
return false;
|
|
68
|
+
entry.controller.abort();
|
|
69
|
+
// Wait for the run to fully settle (the executor's promise, incl. any
|
|
70
|
+
// post-abort write) so callers don't race its teardown.
|
|
71
|
+
await entry.done;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Restore persisted jobs and rebuild their timers. Call once at startup
|
|
76
|
+
* after `setStore`. nextRun is recomputed forward from now — we do NOT
|
|
77
|
+
* catch up on runs missed while the process was down (avoids a restart
|
|
78
|
+
* thundering-herd; aligns with Codex's stateless philosophy). Disabled
|
|
79
|
+
* jobs are restored without a timer.
|
|
80
|
+
*/
|
|
81
|
+
/**
|
|
82
|
+
* Reconcile in-memory jobs against the on-disk store (the store is the source
|
|
83
|
+
* of truth). Safe to call repeatedly — used both at startup and as a periodic
|
|
84
|
+
* re-sync when another process (e.g. the desktop agent worker) may have
|
|
85
|
+
* created/deleted/changed jobs through the same store. Reconciliation:
|
|
86
|
+
* - jobs on disk but not in memory → added (and armed if enabled)
|
|
87
|
+
* - jobs in memory but not on disk → removed + timer cleared
|
|
88
|
+
* - jobs whose schedule/enabled differ → re-armed to match disk
|
|
89
|
+
*
|
|
90
|
+
* Pass `{ arm: false }` in a host that must NOT execute jobs (the worker —
|
|
91
|
+
* separate process from the one that owns execution); it still tracks +
|
|
92
|
+
* persists but starts no timers, so jobs never run twice across processes.
|
|
93
|
+
*/
|
|
94
|
+
loadJobs(opts) {
|
|
95
|
+
if (!this.store)
|
|
96
|
+
return;
|
|
97
|
+
this.reconcileJobs(this.store.load(), opts);
|
|
98
|
+
}
|
|
99
|
+
reconcileJobs(persisted, opts) {
|
|
100
|
+
const arm = opts?.arm ?? true;
|
|
101
|
+
const onDisk = new Map(persisted.map((j) => [j.id, j]));
|
|
102
|
+
// 1. Drop in-memory jobs that no longer exist on disk (deleted elsewhere).
|
|
103
|
+
for (const id of [...this.jobs.keys()]) {
|
|
104
|
+
if (!onDisk.has(id)) {
|
|
105
|
+
this.clearTimer(id);
|
|
106
|
+
this.jobs.delete(id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// 2. Add or update jobs from disk.
|
|
110
|
+
let maxId = 0;
|
|
111
|
+
for (const job of persisted) {
|
|
112
|
+
const prev = this.jobs.get(job.id);
|
|
113
|
+
this.jobs.set(job.id, job);
|
|
114
|
+
const n = parseInt(job.id, 10);
|
|
115
|
+
if (Number.isFinite(n) && n > maxId)
|
|
116
|
+
maxId = n;
|
|
117
|
+
// (Re)arm when execution is on. arm() is idempotent (clears any prior
|
|
118
|
+
// timer first), so calling it for unchanged jobs is harmless; it also
|
|
119
|
+
// refreshes nextRun for display.
|
|
120
|
+
if (arm && job.enabled) {
|
|
121
|
+
this.arm(job);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Disabled (or arm suppressed): ensure no stale timer survives a
|
|
125
|
+
// prior enabled state.
|
|
126
|
+
if (prev && this.timers.has(job.id))
|
|
127
|
+
this.clearTimer(job.id);
|
|
128
|
+
// Still refresh nextRun for display even when not arming.
|
|
129
|
+
if (!arm || !job.enabled)
|
|
130
|
+
this.refreshNextRunForDisplay(job);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
this.nextId = Math.max(this.nextId, maxId + 1);
|
|
134
|
+
}
|
|
135
|
+
nextPersistedId(jobs) {
|
|
136
|
+
let maxId = 0;
|
|
137
|
+
for (const job of jobs) {
|
|
138
|
+
const n = parseInt(job.id, 10);
|
|
139
|
+
if (Number.isFinite(n) && n > maxId)
|
|
140
|
+
maxId = n;
|
|
141
|
+
}
|
|
142
|
+
return String(Math.max(this.nextId, maxId + 1));
|
|
143
|
+
}
|
|
144
|
+
/** Recompute nextRun without arming a timer (for display in disabled/no-arm hosts). */
|
|
145
|
+
refreshNextRunForDisplay(job) {
|
|
146
|
+
if (isCronExpression(job.schedule)) {
|
|
147
|
+
const next = nextCronTime(parseCronExpression(job.schedule), job.timezone ?? "UTC", Date.now());
|
|
148
|
+
job.nextRun = next ?? undefined;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
try {
|
|
152
|
+
job.nextRun = Date.now() + parseSchedule(job.schedule);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
job.nextRun = undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
persist() {
|
|
160
|
+
if (!this.store)
|
|
161
|
+
return;
|
|
162
|
+
try {
|
|
163
|
+
this.store.save([...this.jobs.values()]);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Persistence is best-effort: an unwritable disk should not break the
|
|
167
|
+
// in-memory scheduler. The store logs the failure.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
persistRunStats(job) {
|
|
171
|
+
if (!this.store) {
|
|
172
|
+
this.persist();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
this.store.mutate((jobs) => {
|
|
177
|
+
const idx = jobs.findIndex((j) => j.id === job.id);
|
|
178
|
+
if (idx === -1)
|
|
179
|
+
return { jobs, result: null };
|
|
180
|
+
const next = [...jobs];
|
|
181
|
+
next[idx] = {
|
|
182
|
+
...next[idx],
|
|
183
|
+
lastRun: job.lastRun,
|
|
184
|
+
nextRun: job.nextRun,
|
|
185
|
+
runCount: job.runCount,
|
|
186
|
+
...(job.lastRunId !== undefined ? { lastRunId: job.lastRunId } : {}),
|
|
187
|
+
};
|
|
188
|
+
return { jobs: next, result: null };
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Persistence is best-effort; missed UI metadata must not stop scheduling.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
create(name, schedule, prompt, opts) {
|
|
196
|
+
// Validate the schedule up front (interval or cron expr) so a bad string
|
|
197
|
+
// surfaces at create time, not silently at the first missed tick.
|
|
198
|
+
validateSchedule(schedule, opts?.timezone);
|
|
199
|
+
if (this.store) {
|
|
200
|
+
const tx = this.store.mutate((jobs) => {
|
|
201
|
+
const id = this.nextPersistedId(jobs);
|
|
202
|
+
const job = {
|
|
203
|
+
id,
|
|
204
|
+
name,
|
|
205
|
+
schedule,
|
|
206
|
+
prompt,
|
|
207
|
+
enabled: true,
|
|
208
|
+
runCount: 0,
|
|
209
|
+
createdAt: Date.now(),
|
|
210
|
+
...(opts?.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
211
|
+
...(opts?.timezone !== undefined ? { timezone: opts.timezone } : {}),
|
|
212
|
+
...(opts?.permissionLevel !== undefined ? { permissionLevel: opts.permissionLevel } : {}),
|
|
213
|
+
};
|
|
214
|
+
this.refreshNextRunForDisplay(job);
|
|
215
|
+
return { jobs: [...jobs, job], result: job };
|
|
216
|
+
});
|
|
217
|
+
this.reconcileJobs(tx.jobs);
|
|
218
|
+
return this.jobs.get(tx.result.id) ?? tx.result;
|
|
219
|
+
}
|
|
220
|
+
const id = String(this.nextId++);
|
|
221
|
+
const job = {
|
|
222
|
+
id,
|
|
223
|
+
name,
|
|
224
|
+
schedule,
|
|
225
|
+
prompt,
|
|
226
|
+
enabled: true,
|
|
227
|
+
runCount: 0,
|
|
228
|
+
createdAt: Date.now(),
|
|
229
|
+
...(opts?.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
230
|
+
...(opts?.timezone !== undefined ? { timezone: opts.timezone } : {}),
|
|
231
|
+
...(opts?.permissionLevel !== undefined ? { permissionLevel: opts.permissionLevel } : {}),
|
|
232
|
+
};
|
|
233
|
+
this.jobs.set(id, job);
|
|
234
|
+
this.arm(job);
|
|
235
|
+
this.persist();
|
|
236
|
+
return job;
|
|
237
|
+
}
|
|
238
|
+
delete(id) {
|
|
239
|
+
if (this.store) {
|
|
240
|
+
const tx = this.store.mutate((jobs) => {
|
|
241
|
+
const next = jobs.filter((j) => j.id !== id);
|
|
242
|
+
return { jobs: next, result: next.length !== jobs.length };
|
|
243
|
+
});
|
|
244
|
+
this.reconcileJobs(tx.jobs);
|
|
245
|
+
return tx.result;
|
|
246
|
+
}
|
|
247
|
+
this.clearTimer(id);
|
|
248
|
+
const deleted = this.jobs.delete(id);
|
|
249
|
+
if (deleted)
|
|
250
|
+
this.persist();
|
|
251
|
+
return deleted;
|
|
252
|
+
}
|
|
253
|
+
list() {
|
|
254
|
+
return [...this.jobs.values()];
|
|
255
|
+
}
|
|
256
|
+
get(id) {
|
|
257
|
+
return this.jobs.get(id);
|
|
258
|
+
}
|
|
259
|
+
pause(id) {
|
|
260
|
+
if (this.store) {
|
|
261
|
+
const tx = this.store.mutate((jobs) => {
|
|
262
|
+
let changed = false;
|
|
263
|
+
const next = jobs.map((j) => {
|
|
264
|
+
if (j.id !== id)
|
|
265
|
+
return j;
|
|
266
|
+
changed = true;
|
|
267
|
+
return { ...j, enabled: false };
|
|
268
|
+
});
|
|
269
|
+
return { jobs: next, result: changed };
|
|
270
|
+
});
|
|
271
|
+
this.reconcileJobs(tx.jobs);
|
|
272
|
+
return tx.result;
|
|
273
|
+
}
|
|
274
|
+
const job = this.jobs.get(id);
|
|
275
|
+
if (!job)
|
|
276
|
+
return false;
|
|
277
|
+
job.enabled = false;
|
|
278
|
+
this.clearTimer(id);
|
|
279
|
+
this.persist();
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
resume(id) {
|
|
283
|
+
if (this.store) {
|
|
284
|
+
const tx = this.store.mutate((jobs) => {
|
|
285
|
+
let changed = false;
|
|
286
|
+
const next = jobs.map((j) => {
|
|
287
|
+
if (j.id !== id)
|
|
288
|
+
return j;
|
|
289
|
+
changed = true;
|
|
290
|
+
const job = { ...j, enabled: true };
|
|
291
|
+
this.refreshNextRunForDisplay(job);
|
|
292
|
+
return job;
|
|
293
|
+
});
|
|
294
|
+
return { jobs: next, result: changed };
|
|
295
|
+
});
|
|
296
|
+
this.reconcileJobs(tx.jobs);
|
|
297
|
+
return tx.result;
|
|
298
|
+
}
|
|
299
|
+
const job = this.jobs.get(id);
|
|
300
|
+
if (!job)
|
|
301
|
+
return false;
|
|
302
|
+
job.enabled = true;
|
|
303
|
+
this.arm(job);
|
|
304
|
+
this.persist();
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Edit an existing job's fields (name/prompt/schedule/timezone/cwd/
|
|
309
|
+
* permissionLevel) without recreating it. A changed schedule is validated up
|
|
310
|
+
* front (throws on invalid, leaving the job untouched), then the timer is
|
|
311
|
+
* re-armed so the new schedule takes effect immediately. Enabled state is
|
|
312
|
+
* preserved — a paused job stays paused (no timer). Returns the updated job,
|
|
313
|
+
* or null if the id is unknown.
|
|
314
|
+
*/
|
|
315
|
+
update(id, patch) {
|
|
316
|
+
if (this.store) {
|
|
317
|
+
const tx = this.store.mutate((jobs) => {
|
|
318
|
+
let updated = null;
|
|
319
|
+
const next = jobs.map((j) => {
|
|
320
|
+
if (j.id !== id)
|
|
321
|
+
return j;
|
|
322
|
+
const job = { ...j };
|
|
323
|
+
const nextSchedule = patch.schedule ?? job.schedule;
|
|
324
|
+
const nextTimezone = patch.timezone ?? job.timezone;
|
|
325
|
+
if (patch.schedule !== undefined || patch.timezone !== undefined) {
|
|
326
|
+
validateSchedule(nextSchedule, nextTimezone);
|
|
327
|
+
}
|
|
328
|
+
if (patch.name !== undefined)
|
|
329
|
+
job.name = patch.name;
|
|
330
|
+
if (patch.prompt !== undefined)
|
|
331
|
+
job.prompt = patch.prompt;
|
|
332
|
+
if (patch.schedule !== undefined)
|
|
333
|
+
job.schedule = patch.schedule;
|
|
334
|
+
if (patch.timezone !== undefined)
|
|
335
|
+
job.timezone = patch.timezone;
|
|
336
|
+
if (patch.cwd !== undefined)
|
|
337
|
+
job.cwd = patch.cwd;
|
|
338
|
+
if (patch.permissionLevel !== undefined)
|
|
339
|
+
job.permissionLevel = patch.permissionLevel;
|
|
340
|
+
this.refreshNextRunForDisplay(job);
|
|
341
|
+
updated = job;
|
|
342
|
+
return job;
|
|
343
|
+
});
|
|
344
|
+
return { jobs: next, result: updated };
|
|
345
|
+
});
|
|
346
|
+
this.reconcileJobs(tx.jobs);
|
|
347
|
+
return tx.result ? this.jobs.get(id) ?? tx.result : null;
|
|
348
|
+
}
|
|
349
|
+
const job = this.jobs.get(id);
|
|
350
|
+
if (!job)
|
|
351
|
+
return null;
|
|
352
|
+
// Validate a new schedule/timezone BEFORE mutating anything.
|
|
353
|
+
const nextSchedule = patch.schedule ?? job.schedule;
|
|
354
|
+
const nextTimezone = patch.timezone ?? job.timezone;
|
|
355
|
+
if (patch.schedule !== undefined || patch.timezone !== undefined) {
|
|
356
|
+
validateSchedule(nextSchedule, nextTimezone);
|
|
357
|
+
}
|
|
358
|
+
if (patch.name !== undefined)
|
|
359
|
+
job.name = patch.name;
|
|
360
|
+
if (patch.prompt !== undefined)
|
|
361
|
+
job.prompt = patch.prompt;
|
|
362
|
+
if (patch.schedule !== undefined)
|
|
363
|
+
job.schedule = patch.schedule;
|
|
364
|
+
if (patch.timezone !== undefined)
|
|
365
|
+
job.timezone = patch.timezone;
|
|
366
|
+
if (patch.cwd !== undefined)
|
|
367
|
+
job.cwd = patch.cwd;
|
|
368
|
+
if (patch.permissionLevel !== undefined)
|
|
369
|
+
job.permissionLevel = patch.permissionLevel;
|
|
370
|
+
// Re-arm so a schedule/timezone change drives execution now. arm() is
|
|
371
|
+
// idempotent and respects enabled + executionEnabled (paused → no timer,
|
|
372
|
+
// but nextRun still refreshed for display).
|
|
373
|
+
if (job.enabled) {
|
|
374
|
+
this.arm(job);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
this.clearTimer(id);
|
|
378
|
+
this.refreshNextRunForDisplay(job);
|
|
379
|
+
}
|
|
380
|
+
this.persist();
|
|
381
|
+
return job;
|
|
382
|
+
}
|
|
383
|
+
stopAll() {
|
|
384
|
+
for (const timer of this.timers.values()) {
|
|
385
|
+
clearTimeout(timer);
|
|
386
|
+
}
|
|
387
|
+
this.timers.clear();
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Fire a job immediately, out of band of its schedule (the "Run now" button).
|
|
391
|
+
* Respects the re-entrancy guard (no-op if a run is already in flight) and
|
|
392
|
+
* does not disturb the existing timer. Returns false if the id is unknown.
|
|
393
|
+
*/
|
|
394
|
+
runNow(id) {
|
|
395
|
+
const job = this.jobs.get(id);
|
|
396
|
+
if (!job)
|
|
397
|
+
return false;
|
|
398
|
+
// Run-stat bookkeeping is shared with scheduled fires; nextRun is left as-is
|
|
399
|
+
// (a manual run shouldn't shift the next scheduled occurrence). force=true
|
|
400
|
+
// so a paused job can still be run on demand.
|
|
401
|
+
void this.fire(job, () => { }, true);
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Arm a job's timer. Interval schedules ("5m") use setInterval; cron-
|
|
406
|
+
* expression schedules ("0 9 * * 1-5") compute the next trigger in the
|
|
407
|
+
* job's timezone and use setTimeout, re-arming after each fire. nextRun is
|
|
408
|
+
* updated to reflect the scheduled instant.
|
|
409
|
+
*/
|
|
410
|
+
arm(job) {
|
|
411
|
+
this.clearTimer(job.id);
|
|
412
|
+
if (isCronExpression(job.schedule)) {
|
|
413
|
+
// Compute nextRun for display even when execution is disabled.
|
|
414
|
+
const cron = parseCronExpression(job.schedule);
|
|
415
|
+
const next = nextCronTime(cron, job.timezone ?? "UTC", Date.now());
|
|
416
|
+
job.nextRun = next ?? undefined;
|
|
417
|
+
if (!this.executionEnabled)
|
|
418
|
+
return;
|
|
419
|
+
this.armCron(job);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
const intervalMs = parseSchedule(job.schedule);
|
|
423
|
+
job.nextRun = Date.now() + intervalMs;
|
|
424
|
+
if (!this.executionEnabled)
|
|
425
|
+
return;
|
|
426
|
+
const timer = setInterval(() => void this.fire(job, () => this.refreshIntervalNextRun(job, intervalMs)), intervalMs);
|
|
427
|
+
this.timers.set(job.id, timer);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
refreshIntervalNextRun(job, intervalMs) {
|
|
431
|
+
job.nextRun = Date.now() + intervalMs;
|
|
432
|
+
}
|
|
433
|
+
armCron(job) {
|
|
434
|
+
const cron = parseCronExpression(job.schedule);
|
|
435
|
+
const tz = job.timezone ?? "UTC";
|
|
436
|
+
const next = nextCronTime(cron, tz, Date.now());
|
|
437
|
+
if (next === null) {
|
|
438
|
+
// Unsatisfiable within the search window — leave unscheduled.
|
|
439
|
+
job.nextRun = undefined;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
job.nextRun = next;
|
|
443
|
+
const delay = Math.max(0, next - Date.now());
|
|
444
|
+
const timer = setTimeout(() => {
|
|
445
|
+
void this.fire(job, () => {
|
|
446
|
+
// Re-arm for the following occurrence.
|
|
447
|
+
if (job.enabled)
|
|
448
|
+
this.armCron(job);
|
|
449
|
+
});
|
|
450
|
+
}, delay);
|
|
451
|
+
this.timers.set(job.id, timer);
|
|
452
|
+
}
|
|
453
|
+
clearTimer(id) {
|
|
454
|
+
const t = this.timers.get(id);
|
|
455
|
+
if (t) {
|
|
456
|
+
clearTimeout(t);
|
|
457
|
+
this.timers.delete(id);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Shared fire path for both schedule kinds. Re-entrancy guard, run-stat
|
|
462
|
+
* bookkeeping, persistence, then the executor. `afterStats` updates nextRun
|
|
463
|
+
* for the next occurrence (interval: now+interval; cron: re-armed inside).
|
|
464
|
+
*/
|
|
465
|
+
async fire(job, afterStats, force = false) {
|
|
466
|
+
if (!job.enabled && !force)
|
|
467
|
+
return;
|
|
468
|
+
// Re-entrancy guard: if this job's previous run is still in flight
|
|
469
|
+
// (onExecute slower than the interval), skip rather than stacking
|
|
470
|
+
// overlapping executions that double-count runCount.
|
|
471
|
+
if (this.running.has(job.id))
|
|
472
|
+
return;
|
|
473
|
+
this.running.add(job.id);
|
|
474
|
+
// One in-flight entry per job. abort(jobId) trips `controller` (Engine.run
|
|
475
|
+
// cooperates and resolves early) and awaits `done`, which we resolve in the
|
|
476
|
+
// finally below once the run — including any post-abort write — has settled.
|
|
477
|
+
const controller = new AbortController();
|
|
478
|
+
let resolveDone;
|
|
479
|
+
const done = new Promise((r) => {
|
|
480
|
+
resolveDone = r;
|
|
481
|
+
});
|
|
482
|
+
this.runningControllers.set(job.id, { controller, done });
|
|
483
|
+
job.lastRun = Date.now();
|
|
484
|
+
job.runCount++;
|
|
485
|
+
afterStats();
|
|
486
|
+
// Persist updated run stats so runCount/lastRun/nextRun survive a restart.
|
|
487
|
+
this.persistRunStats(job);
|
|
488
|
+
try {
|
|
489
|
+
await this.onExecute?.(job, controller.signal);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// Job execution failed — continue scheduling.
|
|
493
|
+
}
|
|
494
|
+
finally {
|
|
495
|
+
// The executor may have recorded lastRunId on the job. Persist only run
|
|
496
|
+
// metadata so an old in-flight job cannot overwrite an edited prompt or
|
|
497
|
+
// schedule with its stale copy.
|
|
498
|
+
this.persistRunStats(job);
|
|
499
|
+
this.running.delete(job.id);
|
|
500
|
+
this.runningControllers.delete(job.id);
|
|
501
|
+
// Unblock any abort() awaiting this run's teardown.
|
|
502
|
+
resolveDone();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/** Validate a schedule string (interval or cron expr) without scheduling. Throws on invalid. */
|
|
507
|
+
function validateSchedule(schedule, timezone) {
|
|
508
|
+
if (isCronExpression(schedule)) {
|
|
509
|
+
parseCronExpression(schedule); // throws on bad fields
|
|
510
|
+
// Validate timezone is acceptable to Intl (throws RangeError if not).
|
|
511
|
+
if (timezone !== undefined) {
|
|
512
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone });
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
parseSchedule(schedule); // throws on bad interval
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Parse a schedule string into milliseconds.
|
|
520
|
+
* Supports: "30s", "5m", "1h", "1d", or raw (all-digit) milliseconds.
|
|
521
|
+
*
|
|
522
|
+
* Throws on anything else rather than silently falling back to a default —
|
|
523
|
+
* a typo like "5mn" should surface as an error, not quietly schedule every
|
|
524
|
+
* 10 minutes (review-2026-05-30).
|
|
525
|
+
*/
|
|
526
|
+
function parseSchedule(schedule) {
|
|
527
|
+
const match = schedule.match(/^(\d+)(s|m|h|d)$/);
|
|
528
|
+
if (match) {
|
|
529
|
+
const value = parseInt(match[1], 10);
|
|
530
|
+
switch (match[2]) {
|
|
531
|
+
case "s": return value * 1000;
|
|
532
|
+
case "m": return value * 60 * 1000;
|
|
533
|
+
case "h": return value * 60 * 60 * 1000;
|
|
534
|
+
case "d": return value * 24 * 60 * 60 * 1000;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Raw milliseconds — must be all digits and > 0 (parseInt would otherwise
|
|
538
|
+
// accept "1500abc").
|
|
539
|
+
if (/^\d+$/.test(schedule)) {
|
|
540
|
+
const ms = parseInt(schedule, 10);
|
|
541
|
+
if (ms > 0)
|
|
542
|
+
return ms;
|
|
543
|
+
}
|
|
544
|
+
throw new Error(`Invalid schedule: ${JSON.stringify(schedule)}. Use "30s"/"5m"/"1h"/"1d" or a positive number of milliseconds.`);
|
|
545
|
+
}
|
|
546
|
+
export const cronScheduler = new CronScheduler();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronStore — persistence for cron jobs.
|
|
3
|
+
*
|
|
4
|
+
* Writes a single-file JSON snapshot of all jobs to `~/.code-shell/cron.json`
|
|
5
|
+
* (global by default), so scheduled tasks survive a process restart. Uses the
|
|
6
|
+
* same atomic tmp+rename write as FileRunStore so a crash mid-write can't
|
|
7
|
+
* truncate the file and lose every job.
|
|
8
|
+
*
|
|
9
|
+
* Cross-process writes are serialized with a directory lock. Hosts that need
|
|
10
|
+
* read-modify-write behavior should use mutate() so load + save happen under
|
|
11
|
+
* the same lock and one process cannot overwrite another process's new job
|
|
12
|
+
* with a stale in-memory snapshot.
|
|
13
|
+
*/
|
|
14
|
+
import type { CronJob } from "./scheduler.js";
|
|
15
|
+
/** Default global location. Mirrors FileRunStore's `~/.code-shell/...` layout. */
|
|
16
|
+
export declare function defaultCronStorePath(): string;
|
|
17
|
+
export declare class CronStore {
|
|
18
|
+
private readonly file;
|
|
19
|
+
constructor(file?: string);
|
|
20
|
+
/** Load all persisted jobs. Returns [] when absent or unreadable. */
|
|
21
|
+
load(): CronJob[];
|
|
22
|
+
/**
|
|
23
|
+
* Atomically load, mutate, and save jobs under the store lock. This is the
|
|
24
|
+
* safe path for create/update/delete/pause/resume across the desktop main
|
|
25
|
+
* process and the agent worker process.
|
|
26
|
+
*/
|
|
27
|
+
mutate<T>(fn: (jobs: CronJob[]) => {
|
|
28
|
+
jobs: CronJob[];
|
|
29
|
+
result: T;
|
|
30
|
+
}): {
|
|
31
|
+
jobs: CronJob[];
|
|
32
|
+
result: T;
|
|
33
|
+
};
|
|
34
|
+
/** Persist the full job set. Atomic: stage to .tmp, then rename. */
|
|
35
|
+
save(jobs: CronJob[]): void;
|
|
36
|
+
private loadUnlocked;
|
|
37
|
+
private saveUnlocked;
|
|
38
|
+
private acquireStoreLock;
|
|
39
|
+
}
|