matrx-scheduler 0.3.0__tar.gz
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.
- matrx_scheduler-0.3.0/.gitignore +257 -0
- matrx_scheduler-0.3.0/CLAUDE.md +103 -0
- matrx_scheduler-0.3.0/PKG-INFO +144 -0
- matrx_scheduler-0.3.0/README.md +109 -0
- matrx_scheduler-0.3.0/matrx_scheduler/__init__.py +85 -0
- matrx_scheduler-0.3.0/matrx_scheduler/_ext.py +99 -0
- matrx_scheduler-0.3.0/matrx_scheduler/api/__init__.py +64 -0
- matrx_scheduler-0.3.0/matrx_scheduler/api/per_request.py +144 -0
- matrx_scheduler-0.3.0/matrx_scheduler/api/router_scheduler.py +749 -0
- matrx_scheduler-0.3.0/matrx_scheduler/api/schemas.py +261 -0
- matrx_scheduler-0.3.0/matrx_scheduler/api/user_queries.py +346 -0
- matrx_scheduler-0.3.0/matrx_scheduler/cron_helpers.py +76 -0
- matrx_scheduler-0.3.0/matrx_scheduler/models.py +170 -0
- matrx_scheduler-0.3.0/matrx_scheduler/next_due.py +78 -0
- matrx_scheduler-0.3.0/matrx_scheduler/queries.py +422 -0
- matrx_scheduler-0.3.0/matrx_scheduler/runner.py +367 -0
- matrx_scheduler-0.3.0/matrx_scheduler/scanner.py +225 -0
- matrx_scheduler-0.3.0/pyproject.toml +50 -0
- matrx_scheduler-0.3.0/tests/__init__.py +0 -0
- matrx_scheduler-0.3.0/tests/conftest.py +462 -0
- matrx_scheduler-0.3.0/tests/test_api_cron_status.py +191 -0
- matrx_scheduler-0.3.0/tests/test_api_per_request.py +143 -0
- matrx_scheduler-0.3.0/tests/test_api_tasks.py +308 -0
- matrx_scheduler-0.3.0/tests/test_api_triggers_runs.py +178 -0
- matrx_scheduler-0.3.0/tests/test_cron_helpers.py +49 -0
- matrx_scheduler-0.3.0/tests/test_edge_cases.py +118 -0
- matrx_scheduler-0.3.0/tests/test_next_due.py +60 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
*.pyc
|
|
2
|
+
secrets/
|
|
3
|
+
ignore/
|
|
4
|
+
temp/
|
|
5
|
+
logs/
|
|
6
|
+
todo
|
|
7
|
+
text_notes/
|
|
8
|
+
aidream/secrets/2.env
|
|
9
|
+
automation_matrix/matrix_processing/temp/*
|
|
10
|
+
cd
|
|
11
|
+
# Byte-compiled / optimized / DLL files
|
|
12
|
+
__pycache__/
|
|
13
|
+
*.py[cod]
|
|
14
|
+
*$py.class
|
|
15
|
+
|
|
16
|
+
# C extensions
|
|
17
|
+
*.so
|
|
18
|
+
.venv/
|
|
19
|
+
|
|
20
|
+
# Distribution / packaging
|
|
21
|
+
.Python
|
|
22
|
+
build/
|
|
23
|
+
develop-eggs/
|
|
24
|
+
dist/
|
|
25
|
+
downloads/
|
|
26
|
+
eggs/
|
|
27
|
+
.eggs/
|
|
28
|
+
lib/
|
|
29
|
+
lib64/
|
|
30
|
+
# The blanket lib/ rule above is from the standard Python .gitignore template
|
|
31
|
+
# and was silently swallowing TS source under the SPA `src/lib/` folders.
|
|
32
|
+
# Re-allow them explicitly so frontend builds don't ship without their lib layer.
|
|
33
|
+
!dashboard/src/lib/
|
|
34
|
+
!dashboard/src/lib/**
|
|
35
|
+
!workflow-studio/src/lib/
|
|
36
|
+
!workflow-studio/src/lib/**
|
|
37
|
+
parts/
|
|
38
|
+
sdist/
|
|
39
|
+
var/
|
|
40
|
+
wheels/
|
|
41
|
+
share/python-wheels/
|
|
42
|
+
*.egg-info/
|
|
43
|
+
.installed.cfg
|
|
44
|
+
*.egg
|
|
45
|
+
MANIFEST
|
|
46
|
+
|
|
47
|
+
# PyInstaller
|
|
48
|
+
# Usually these files are written by a python script from a template
|
|
49
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
50
|
+
*.manifest
|
|
51
|
+
*.spec
|
|
52
|
+
|
|
53
|
+
# Installer logs
|
|
54
|
+
pip-log.txt
|
|
55
|
+
pip-delete-this-directory.txt
|
|
56
|
+
|
|
57
|
+
# Unit test / coverage reports
|
|
58
|
+
ai/tests/clean_response.json
|
|
59
|
+
ai/tests/cx_storage_response.json
|
|
60
|
+
ai/tests/execution_test.py
|
|
61
|
+
ai/tests/final_response.json
|
|
62
|
+
htmlcov/
|
|
63
|
+
.tox/
|
|
64
|
+
.nox/
|
|
65
|
+
.coverage
|
|
66
|
+
.coverage.*
|
|
67
|
+
.cache
|
|
68
|
+
nosetests.xml
|
|
69
|
+
coverage.xml
|
|
70
|
+
*.cover
|
|
71
|
+
*.py,cover
|
|
72
|
+
.hypothesis/
|
|
73
|
+
.pytest_cache/
|
|
74
|
+
cover/
|
|
75
|
+
|
|
76
|
+
# Translations
|
|
77
|
+
*.mo
|
|
78
|
+
*.pot
|
|
79
|
+
|
|
80
|
+
# Django stuff:
|
|
81
|
+
*.log
|
|
82
|
+
local_settings.py
|
|
83
|
+
db.sqlite3
|
|
84
|
+
db.sqlite3-journal
|
|
85
|
+
|
|
86
|
+
# Flask stuff:
|
|
87
|
+
instance/
|
|
88
|
+
.webassets-cache
|
|
89
|
+
|
|
90
|
+
# Scrapy stuff:
|
|
91
|
+
.scrapy
|
|
92
|
+
|
|
93
|
+
# Sphinx documentation
|
|
94
|
+
docs/_build/
|
|
95
|
+
|
|
96
|
+
# PyBuilder
|
|
97
|
+
.pybuilder/
|
|
98
|
+
target/
|
|
99
|
+
|
|
100
|
+
# Jupyter Notebook
|
|
101
|
+
.ipynb_checkpoints
|
|
102
|
+
|
|
103
|
+
# IPython
|
|
104
|
+
profile_default/
|
|
105
|
+
ipython_config.py
|
|
106
|
+
|
|
107
|
+
# pyenv
|
|
108
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
109
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
110
|
+
# .python-version
|
|
111
|
+
|
|
112
|
+
# pipenv
|
|
113
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
114
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
115
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
116
|
+
# install all needed dependencies.
|
|
117
|
+
#Pipfile.lock
|
|
118
|
+
|
|
119
|
+
# poetry
|
|
120
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
121
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
122
|
+
# commonly ignored for libraries.
|
|
123
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
124
|
+
|
|
125
|
+
# pdm
|
|
126
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
127
|
+
#pdm.lock
|
|
128
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
129
|
+
# in version control.
|
|
130
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
131
|
+
.pdm.toml
|
|
132
|
+
|
|
133
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
134
|
+
__pypackages__/
|
|
135
|
+
|
|
136
|
+
# Celery stuff
|
|
137
|
+
celerybeat-schedule
|
|
138
|
+
celerybeat.pid
|
|
139
|
+
|
|
140
|
+
# SageMath parsed files
|
|
141
|
+
*.sage.py
|
|
142
|
+
|
|
143
|
+
# Environments
|
|
144
|
+
.env
|
|
145
|
+
.env_remote
|
|
146
|
+
.venv
|
|
147
|
+
env/
|
|
148
|
+
venv/
|
|
149
|
+
ENV/
|
|
150
|
+
env.bak/
|
|
151
|
+
venv.bak/
|
|
152
|
+
.env.armanonly
|
|
153
|
+
|
|
154
|
+
# Spyder project settings
|
|
155
|
+
.spyderproject
|
|
156
|
+
.spyproject
|
|
157
|
+
|
|
158
|
+
# Rope project settings
|
|
159
|
+
.ropeproject
|
|
160
|
+
|
|
161
|
+
# mkdocs documentation
|
|
162
|
+
/site
|
|
163
|
+
|
|
164
|
+
# mypy
|
|
165
|
+
.mypy_cache/
|
|
166
|
+
.dmypy.json
|
|
167
|
+
dmypy.json
|
|
168
|
+
|
|
169
|
+
# Pyre type checker
|
|
170
|
+
.pyre/
|
|
171
|
+
|
|
172
|
+
# random armani files
|
|
173
|
+
/armani_dev/secrets/
|
|
174
|
+
/armani/
|
|
175
|
+
/_armani/
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# pytype static type analyzer
|
|
180
|
+
.pytype/
|
|
181
|
+
|
|
182
|
+
# Cython debug symbols
|
|
183
|
+
cython_debug/
|
|
184
|
+
|
|
185
|
+
.idea/
|
|
186
|
+
.vscode/
|
|
187
|
+
/node_modules/
|
|
188
|
+
|
|
189
|
+
dump.rdb
|
|
190
|
+
|
|
191
|
+
frontend/
|
|
192
|
+
|
|
193
|
+
# AME Temp Files and directory structure
|
|
194
|
+
# Ignore all files in the temp directory and its subdirectories
|
|
195
|
+
/temp/**/*
|
|
196
|
+
/tmp/**/*
|
|
197
|
+
|
|
198
|
+
# Allow .gitkeep files to retain directory structure
|
|
199
|
+
!/temp/**/.gitkeep
|
|
200
|
+
!/tmp/**/.gitkeep
|
|
201
|
+
|
|
202
|
+
# Armani
|
|
203
|
+
.history*
|
|
204
|
+
.history/
|
|
205
|
+
local_data/
|
|
206
|
+
local_reports_data/
|
|
207
|
+
webscraper/quick_scrapes/temp/
|
|
208
|
+
automation_matrix/ai_apis/fireworks/_dev/*
|
|
209
|
+
automation_matrix/ai_apis/fireworks/_dev/fireworks_sample.py
|
|
210
|
+
*.pdf
|
|
211
|
+
*.flac
|
|
212
|
+
*.mp3
|
|
213
|
+
*.wav
|
|
214
|
+
miniconda.sh
|
|
215
|
+
/database/python_sql/temp_data/
|
|
216
|
+
.history*
|
|
217
|
+
.history/
|
|
218
|
+
.history/
|
|
219
|
+
|
|
220
|
+
_dev/
|
|
221
|
+
/_dev/
|
|
222
|
+
requirements_filtered.txt
|
|
223
|
+
|
|
224
|
+
# matrx-dev-tools backups
|
|
225
|
+
.env-backups/
|
|
226
|
+
# Matrx Ship config (contains API key)
|
|
227
|
+
.matrx-ship.json
|
|
228
|
+
|
|
229
|
+
# Matrx config (contains API keys)
|
|
230
|
+
.matrx.json
|
|
231
|
+
.matrx-tools.conf
|
|
232
|
+
|
|
233
|
+
# Claude Code local worktrees and per-user settings
|
|
234
|
+
.claude/worktrees/
|
|
235
|
+
.claude/settings.local.json
|
|
236
|
+
|
|
237
|
+
# Append-only snapshots from matrx_utils.update_history (unbounded; do not commit)
|
|
238
|
+
common/utils/data_in_code/data_history.json
|
|
239
|
+
packages/matrx-utils/matrx_utils/data_in_code/data_history.json
|
|
240
|
+
|
|
241
|
+
# Tool-dispatch debug logs — one file per server start, never committed
|
|
242
|
+
.matrx-debug/
|
|
243
|
+
|
|
244
|
+
# macOS Finder metadata
|
|
245
|
+
.DS_Store
|
|
246
|
+
**/.DS_Store
|
|
247
|
+
|
|
248
|
+
# Environment files
|
|
249
|
+
.env
|
|
250
|
+
.env.*
|
|
251
|
+
*.env
|
|
252
|
+
*.env.*
|
|
253
|
+
|
|
254
|
+
# Keep safe templates trackable
|
|
255
|
+
!.env.example
|
|
256
|
+
!.env.sample
|
|
257
|
+
!.env.template
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# matrx-scheduler — CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Standalone PyPI-publishable Python package. Server-side executor for the
|
|
4
|
+
`sch_*` scheduling spine shared with matrx-frontend and matrx-extend.
|
|
5
|
+
|
|
6
|
+
## Package independence
|
|
7
|
+
|
|
8
|
+
Per `aidream/CLAUDE.md` (top-level): packages MUST NOT import from `aidream/`
|
|
9
|
+
or root-level modules. matrx-scheduler reads `_ext.py` for host-supplied
|
|
10
|
+
dependencies via `configure(...)` — never reaches out.
|
|
11
|
+
|
|
12
|
+
Dependency graph (inter-package):
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
matrx-utils (foundation — no matrx deps)
|
|
16
|
+
▲
|
|
17
|
+
matrx-scheduler (needs: matrx-utils + host-injected
|
|
18
|
+
supabase / context / agent_runner)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Public API
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import matrx_scheduler
|
|
25
|
+
|
|
26
|
+
matrx_scheduler.configure(
|
|
27
|
+
supabase_client=..., # supabase-py async client
|
|
28
|
+
surface="server",
|
|
29
|
+
agent_runner=..., # async callable(AgentRunInput) -> AgentRunResult
|
|
30
|
+
get_app_context=..., # () -> AppContext | None
|
|
31
|
+
scan_interval_seconds=5.0,
|
|
32
|
+
lease_seconds=600,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Background loop
|
|
36
|
+
await matrx_scheduler.start_scanner()
|
|
37
|
+
await matrx_scheduler.stop_scanner()
|
|
38
|
+
|
|
39
|
+
# Cron helpers (used by the /scheduling/* routes)
|
|
40
|
+
matrx_scheduler.validate_cron("0 9 * * 1-5", "America/Los_Angeles")
|
|
41
|
+
matrx_scheduler.next_n_fires("0 9 * * 1-5", "UTC", n=5)
|
|
42
|
+
matrx_scheduler.compute_next_due_at("interval", {"every_seconds": 3600})
|
|
43
|
+
|
|
44
|
+
# Health
|
|
45
|
+
status: ScannerStatus = matrx_scheduler.status()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Module layout
|
|
49
|
+
|
|
50
|
+
- `__init__.py` — public API exports
|
|
51
|
+
- `_ext.py` — host-injection registry (`configure`, `get_ext`)
|
|
52
|
+
- `models.py` — Pydantic models for sch_task / sch_agent_task / sch_trigger /
|
|
53
|
+
sch_run + the AgentRunInput/Result contract
|
|
54
|
+
- `queries.py` — Supabase access (the only place that calls `.table('sch_*')`)
|
|
55
|
+
- `scanner.py` — background asyncio loop
|
|
56
|
+
- `runner.py` — drives a single claimed run end-to-end
|
|
57
|
+
- `next_due.py` — `compute_next_due_at(...)` per trigger type
|
|
58
|
+
- `cron_helpers.py` — `croniter`-backed validators
|
|
59
|
+
|
|
60
|
+
## Invariants
|
|
61
|
+
|
|
62
|
+
1. **Atomic claim, not "read-then-write".** Scanner finds candidates, then
|
|
63
|
+
each call to `queries.claim_task` does the concurrency check (active runs
|
|
64
|
+
count) and inserts the sch_run row in one go. Multiple scanners racing
|
|
65
|
+
the same task can't double-run because `max_concurrent` is enforced
|
|
66
|
+
inside the claim.
|
|
67
|
+
2. **DB owns `sch_task.next_due_at`.** After every recurring fire, the
|
|
68
|
+
scheduler calls `advance_trigger_next_due_at` on `sch_trigger`; the DB
|
|
69
|
+
trigger `sch_trigger_cascade_next_due_at` updates `sch_task.next_due_at`.
|
|
70
|
+
Don't UPDATE the parent column directly from the package.
|
|
71
|
+
3. **Lease expiry sweeps run every tick.** Crashed runners leave behind
|
|
72
|
+
`status IN (claimed,running) AND claim_expires_at < now()` rows. Scanner
|
|
73
|
+
marks them `failed` so the trigger's next firing can re-enqueue.
|
|
74
|
+
4. **Heartbeat conversation continuity.** First run of a heartbeat trigger
|
|
75
|
+
sets `sch_agent_task.persistent_conversation_id` from the runner's
|
|
76
|
+
`result.conversation_id`. Subsequent runs use it. Don't recreate.
|
|
77
|
+
5. **Host-injected agent_runner is fire-and-forget from the scheduler's POV.**
|
|
78
|
+
It must complete or raise; the scheduler doesn't poll progress.
|
|
79
|
+
Inside the runner, use matrx-connect's streaming infra normally.
|
|
80
|
+
|
|
81
|
+
## Where this is wired
|
|
82
|
+
|
|
83
|
+
- aidream `package_integration.py::_configure_matrx_scheduler` — startup config.
|
|
84
|
+
- aidream `api/app.py` — `start_scanner()` in lifespan when
|
|
85
|
+
`AIDREAM_SCHEDULER=1`. Stops on shutdown.
|
|
86
|
+
- aidream `api/routers/scheduling.py` — 5 HTTP endpoints under `/scheduling`
|
|
87
|
+
consumed by matrx-frontend.
|
|
88
|
+
|
|
89
|
+
## Tests
|
|
90
|
+
|
|
91
|
+
`tests/test_next_due.py` and `tests/test_cron_helpers.py` cover all 5 trigger
|
|
92
|
+
types and the cron parser. Run with:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
uv run pytest packages/matrx-scheduler/tests
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Related
|
|
99
|
+
|
|
100
|
+
- Spec: matrx-frontend `docs/SCHEDULING.md`
|
|
101
|
+
- Migrations: matrx-frontend `migrations/sch_*.sql` (the spine is shared)
|
|
102
|
+
- FE control plane: matrx-frontend `features/scheduling/FEATURE.md`
|
|
103
|
+
- Browser-context executor: matrx-extend `src/lib/agenda/*`
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: matrx-scheduler
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Server-side execution engine for the matrx scheduling spine (sch_* tables): scanner, claim, runner, cron parser, HTTP API.
|
|
5
|
+
Author-email: Matrx <admin@aimatrx.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: agents,cron,matrx,scheduler,supabase
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
|
+
Requires-Dist: croniter>=2.0
|
|
16
|
+
Requires-Dist: matrx-utils
|
|
17
|
+
Requires-Dist: pydantic>=2.12
|
|
18
|
+
Requires-Dist: supabase>=2.0
|
|
19
|
+
Provides-Extra: api
|
|
20
|
+
Requires-Dist: fastapi>=0.115; extra == 'api'
|
|
21
|
+
Requires-Dist: httpx>=0.27; extra == 'api'
|
|
22
|
+
Requires-Dist: matrx-connect; extra == 'api'
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: fastapi>=0.115; extra == 'dev'
|
|
25
|
+
Requires-Dist: freezegun>=1.4; extra == 'dev'
|
|
26
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
27
|
+
Requires-Dist: matrx-connect; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Provides-Extra: host
|
|
31
|
+
Requires-Dist: matrx-ai; extra == 'host'
|
|
32
|
+
Requires-Dist: matrx-connect; extra == 'host'
|
|
33
|
+
Requires-Dist: matrx-orm; extra == 'host'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# matrx-scheduler
|
|
37
|
+
|
|
38
|
+
Server-side execution engine for the matrx scheduling spine (`sch_*` tables in
|
|
39
|
+
Supabase). Provides:
|
|
40
|
+
|
|
41
|
+
- a scanner loop that claims due tasks atomically,
|
|
42
|
+
- a runner that drives the host's agent_runner / tool_runner,
|
|
43
|
+
- an authoritative cron parser + next-due computation,
|
|
44
|
+
- and (since 0.3) an HTTP API surface (FastAPI) for task / trigger / run CRUD,
|
|
45
|
+
manual fires, scanner status, and cron preview.
|
|
46
|
+
|
|
47
|
+
Any host application (aidream is the reference) wires it up via `configure()`
|
|
48
|
+
and either runs the scanner directly, mounts the HTTP routes, or both.
|
|
49
|
+
|
|
50
|
+
## What it does (under the hood)
|
|
51
|
+
|
|
52
|
+
- Polls `sch_task` for rows where `next_due_at <= now()` and the host surface
|
|
53
|
+
is in `surfaces[]` (or `'any'`).
|
|
54
|
+
- Atomically claims a candidate by inserting a `sch_run` with `claim_token`
|
|
55
|
+
and `claim_expires_at` (the lease).
|
|
56
|
+
- Drives the matrx-ai agent runner (host-injected) against the claimed task,
|
|
57
|
+
or the host's tool_runner for `kind='tool'`.
|
|
58
|
+
- Writes back results (`status`, `result_summary`, `output_ref`, etc.).
|
|
59
|
+
- Recomputes the next fire time on recurring triggers; the DB cascade
|
|
60
|
+
updates `sch_task.next_due_at` automatically.
|
|
61
|
+
|
|
62
|
+
## Capability-within, injection-without
|
|
63
|
+
|
|
64
|
+
`matrx-scheduler` doesn't import from a host app. It exposes a `configure()`
|
|
65
|
+
function the host calls at startup, passing in:
|
|
66
|
+
|
|
67
|
+
- `supabase_client` — service-role client used by the SCANNER to read sch_task
|
|
68
|
+
across users and update lease state.
|
|
69
|
+
- `surface` — the string this host identifies as in `sch_task.surfaces[]`
|
|
70
|
+
(e.g. `'server'` for aidream, `'desktop'` for matrx-local).
|
|
71
|
+
- `agent_runner` — callable that runs an agent (kind='agent' tasks).
|
|
72
|
+
- `tool_runner` — optional callable for kind='tool' tasks. Hosts that don't
|
|
73
|
+
claim tool tasks (e.g. aidream itself) leave this unset.
|
|
74
|
+
- `user_supabase_factory` — optional callable `(user_jwt) -> AsyncClient`
|
|
75
|
+
used by the HTTP routes to build per-request user-scoped clients. If not
|
|
76
|
+
provided, the package falls back to an env-based factory keyed on
|
|
77
|
+
`SUPABASE_MATRIX_URL` + `SUPABASE_MATRIX_PUBLISHABLE_KEY`.
|
|
78
|
+
- `get_app_context`, `emitter_factory`, `scan_interval_seconds`,
|
|
79
|
+
`lease_seconds` — see the `_ext.configure` docstring.
|
|
80
|
+
|
|
81
|
+
Install with the `host` extra (`pip install matrx-scheduler[host]`) when
|
|
82
|
+
using it inside a full host app. Add the `api` extra
|
|
83
|
+
(`pip install matrx-scheduler[api]`) to mount the HTTP routes.
|
|
84
|
+
|
|
85
|
+
## HTTP API
|
|
86
|
+
|
|
87
|
+
Available since 0.3. Optional — drop the import if you only want the scanner.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from fastapi import FastAPI
|
|
91
|
+
import matrx_scheduler
|
|
92
|
+
|
|
93
|
+
app = FastAPI()
|
|
94
|
+
|
|
95
|
+
# Host startup wiring (any host).
|
|
96
|
+
matrx_scheduler.configure(
|
|
97
|
+
supabase_client=service_role_client,
|
|
98
|
+
surface="server", # or "desktop", "extension", ...
|
|
99
|
+
agent_runner=my_agent_runner,
|
|
100
|
+
user_supabase_factory=my_user_client_factory, # optional
|
|
101
|
+
)
|
|
102
|
+
await matrx_scheduler.start_scanner()
|
|
103
|
+
|
|
104
|
+
# Mount the HTTP routes.
|
|
105
|
+
matrx_scheduler.api.include_routers(app, prefix="/scheduler")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Endpoints
|
|
109
|
+
|
|
110
|
+
All routes use `matrx_connect.AppContext` via `Depends(context_dep)`. Every
|
|
111
|
+
write goes through a per-request Supabase client built from the caller's JWT;
|
|
112
|
+
RLS is the only line of authority on row ownership. The service-role client
|
|
113
|
+
(used by the scanner) is **never** accessible from these routes.
|
|
114
|
+
|
|
115
|
+
| Method | Path | Purpose |
|
|
116
|
+
| ------ | --------------------------------------- | ---------------------------------------------------- |
|
|
117
|
+
| POST | `/scheduler/tasks` | Create a task (optionally with agent_task + trigger) |
|
|
118
|
+
| GET | `/scheduler/tasks` | List tasks (filter by kind, enabled) |
|
|
119
|
+
| GET | `/scheduler/tasks/{id}` | Get task hydrated with agent_task, triggers, runs |
|
|
120
|
+
| PATCH | `/scheduler/tasks/{id}` | Patch task fields (title, enabled, tags, etc.) |
|
|
121
|
+
| DELETE | `/scheduler/tasks/{id}` | Soft-delete (set enabled=false) |
|
|
122
|
+
| POST | `/scheduler/tasks/{id}/run-now` | Enqueue a manual run via `sch_enqueue_manual_run` |
|
|
123
|
+
| GET | `/scheduler/triggers?task_id=...` | List triggers for a task |
|
|
124
|
+
| POST | `/scheduler/triggers` | Create a trigger on an existing task |
|
|
125
|
+
| PATCH | `/scheduler/triggers/{id}` | Patch a trigger (recomputes next_due_at if type/config changed) |
|
|
126
|
+
| DELETE | `/scheduler/triggers/{id}` | Hard-delete a trigger |
|
|
127
|
+
| GET | `/scheduler/runs?task_id=...&status=...`| List run history |
|
|
128
|
+
| GET | `/scheduler/runs/{id}` | Get one run |
|
|
129
|
+
| POST | `/scheduler/cron/validate` | Validate a cron expression + preview next N fires |
|
|
130
|
+
| POST | `/scheduler/cron/preview-fires` | Preview next-fire times for any trigger config |
|
|
131
|
+
| POST | `/scheduler/compute-next-due-at` | Compute the single next due_at for a trigger config |
|
|
132
|
+
| GET | `/scheduler/status` | Scanner health (admin only) |
|
|
133
|
+
|
|
134
|
+
### Co-existence with aidream's `/scheduling/*`
|
|
135
|
+
|
|
136
|
+
aidream had a small router at `/scheduling/*` (cron validation, run-now,
|
|
137
|
+
admin force-disable) before this surface existed. The package routes use
|
|
138
|
+
`/scheduler/*` (singular) so they don't collide. aidream can mount both
|
|
139
|
+
simultaneously, or migrate clients to the package routes opportunistically.
|
|
140
|
+
matrx-local mounts only the package routes; it has no `/scheduling/*` routes
|
|
141
|
+
to begin with.
|
|
142
|
+
|
|
143
|
+
See `docs/SCHEDULING.md` in the matrx-frontend repo for the full data-model
|
|
144
|
+
spec, trigger taxonomy, and lifecycle.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# matrx-scheduler
|
|
2
|
+
|
|
3
|
+
Server-side execution engine for the matrx scheduling spine (`sch_*` tables in
|
|
4
|
+
Supabase). Provides:
|
|
5
|
+
|
|
6
|
+
- a scanner loop that claims due tasks atomically,
|
|
7
|
+
- a runner that drives the host's agent_runner / tool_runner,
|
|
8
|
+
- an authoritative cron parser + next-due computation,
|
|
9
|
+
- and (since 0.3) an HTTP API surface (FastAPI) for task / trigger / run CRUD,
|
|
10
|
+
manual fires, scanner status, and cron preview.
|
|
11
|
+
|
|
12
|
+
Any host application (aidream is the reference) wires it up via `configure()`
|
|
13
|
+
and either runs the scanner directly, mounts the HTTP routes, or both.
|
|
14
|
+
|
|
15
|
+
## What it does (under the hood)
|
|
16
|
+
|
|
17
|
+
- Polls `sch_task` for rows where `next_due_at <= now()` and the host surface
|
|
18
|
+
is in `surfaces[]` (or `'any'`).
|
|
19
|
+
- Atomically claims a candidate by inserting a `sch_run` with `claim_token`
|
|
20
|
+
and `claim_expires_at` (the lease).
|
|
21
|
+
- Drives the matrx-ai agent runner (host-injected) against the claimed task,
|
|
22
|
+
or the host's tool_runner for `kind='tool'`.
|
|
23
|
+
- Writes back results (`status`, `result_summary`, `output_ref`, etc.).
|
|
24
|
+
- Recomputes the next fire time on recurring triggers; the DB cascade
|
|
25
|
+
updates `sch_task.next_due_at` automatically.
|
|
26
|
+
|
|
27
|
+
## Capability-within, injection-without
|
|
28
|
+
|
|
29
|
+
`matrx-scheduler` doesn't import from a host app. It exposes a `configure()`
|
|
30
|
+
function the host calls at startup, passing in:
|
|
31
|
+
|
|
32
|
+
- `supabase_client` — service-role client used by the SCANNER to read sch_task
|
|
33
|
+
across users and update lease state.
|
|
34
|
+
- `surface` — the string this host identifies as in `sch_task.surfaces[]`
|
|
35
|
+
(e.g. `'server'` for aidream, `'desktop'` for matrx-local).
|
|
36
|
+
- `agent_runner` — callable that runs an agent (kind='agent' tasks).
|
|
37
|
+
- `tool_runner` — optional callable for kind='tool' tasks. Hosts that don't
|
|
38
|
+
claim tool tasks (e.g. aidream itself) leave this unset.
|
|
39
|
+
- `user_supabase_factory` — optional callable `(user_jwt) -> AsyncClient`
|
|
40
|
+
used by the HTTP routes to build per-request user-scoped clients. If not
|
|
41
|
+
provided, the package falls back to an env-based factory keyed on
|
|
42
|
+
`SUPABASE_MATRIX_URL` + `SUPABASE_MATRIX_PUBLISHABLE_KEY`.
|
|
43
|
+
- `get_app_context`, `emitter_factory`, `scan_interval_seconds`,
|
|
44
|
+
`lease_seconds` — see the `_ext.configure` docstring.
|
|
45
|
+
|
|
46
|
+
Install with the `host` extra (`pip install matrx-scheduler[host]`) when
|
|
47
|
+
using it inside a full host app. Add the `api` extra
|
|
48
|
+
(`pip install matrx-scheduler[api]`) to mount the HTTP routes.
|
|
49
|
+
|
|
50
|
+
## HTTP API
|
|
51
|
+
|
|
52
|
+
Available since 0.3. Optional — drop the import if you only want the scanner.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from fastapi import FastAPI
|
|
56
|
+
import matrx_scheduler
|
|
57
|
+
|
|
58
|
+
app = FastAPI()
|
|
59
|
+
|
|
60
|
+
# Host startup wiring (any host).
|
|
61
|
+
matrx_scheduler.configure(
|
|
62
|
+
supabase_client=service_role_client,
|
|
63
|
+
surface="server", # or "desktop", "extension", ...
|
|
64
|
+
agent_runner=my_agent_runner,
|
|
65
|
+
user_supabase_factory=my_user_client_factory, # optional
|
|
66
|
+
)
|
|
67
|
+
await matrx_scheduler.start_scanner()
|
|
68
|
+
|
|
69
|
+
# Mount the HTTP routes.
|
|
70
|
+
matrx_scheduler.api.include_routers(app, prefix="/scheduler")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Endpoints
|
|
74
|
+
|
|
75
|
+
All routes use `matrx_connect.AppContext` via `Depends(context_dep)`. Every
|
|
76
|
+
write goes through a per-request Supabase client built from the caller's JWT;
|
|
77
|
+
RLS is the only line of authority on row ownership. The service-role client
|
|
78
|
+
(used by the scanner) is **never** accessible from these routes.
|
|
79
|
+
|
|
80
|
+
| Method | Path | Purpose |
|
|
81
|
+
| ------ | --------------------------------------- | ---------------------------------------------------- |
|
|
82
|
+
| POST | `/scheduler/tasks` | Create a task (optionally with agent_task + trigger) |
|
|
83
|
+
| GET | `/scheduler/tasks` | List tasks (filter by kind, enabled) |
|
|
84
|
+
| GET | `/scheduler/tasks/{id}` | Get task hydrated with agent_task, triggers, runs |
|
|
85
|
+
| PATCH | `/scheduler/tasks/{id}` | Patch task fields (title, enabled, tags, etc.) |
|
|
86
|
+
| DELETE | `/scheduler/tasks/{id}` | Soft-delete (set enabled=false) |
|
|
87
|
+
| POST | `/scheduler/tasks/{id}/run-now` | Enqueue a manual run via `sch_enqueue_manual_run` |
|
|
88
|
+
| GET | `/scheduler/triggers?task_id=...` | List triggers for a task |
|
|
89
|
+
| POST | `/scheduler/triggers` | Create a trigger on an existing task |
|
|
90
|
+
| PATCH | `/scheduler/triggers/{id}` | Patch a trigger (recomputes next_due_at if type/config changed) |
|
|
91
|
+
| DELETE | `/scheduler/triggers/{id}` | Hard-delete a trigger |
|
|
92
|
+
| GET | `/scheduler/runs?task_id=...&status=...`| List run history |
|
|
93
|
+
| GET | `/scheduler/runs/{id}` | Get one run |
|
|
94
|
+
| POST | `/scheduler/cron/validate` | Validate a cron expression + preview next N fires |
|
|
95
|
+
| POST | `/scheduler/cron/preview-fires` | Preview next-fire times for any trigger config |
|
|
96
|
+
| POST | `/scheduler/compute-next-due-at` | Compute the single next due_at for a trigger config |
|
|
97
|
+
| GET | `/scheduler/status` | Scanner health (admin only) |
|
|
98
|
+
|
|
99
|
+
### Co-existence with aidream's `/scheduling/*`
|
|
100
|
+
|
|
101
|
+
aidream had a small router at `/scheduling/*` (cron validation, run-now,
|
|
102
|
+
admin force-disable) before this surface existed. The package routes use
|
|
103
|
+
`/scheduler/*` (singular) so they don't collide. aidream can mount both
|
|
104
|
+
simultaneously, or migrate clients to the package routes opportunistically.
|
|
105
|
+
matrx-local mounts only the package routes; it has no `/scheduling/*` routes
|
|
106
|
+
to begin with.
|
|
107
|
+
|
|
108
|
+
See `docs/SCHEDULING.md` in the matrx-frontend repo for the full data-model
|
|
109
|
+
spec, trigger taxonomy, and lifecycle.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
matrx-scheduler — server-side execution engine for the sch_* scheduling spine.
|
|
3
|
+
|
|
4
|
+
Public API:
|
|
5
|
+
configure(...) — wire host-supplied dependencies (call at startup)
|
|
6
|
+
start_scanner() — spawn the background scanner asyncio task
|
|
7
|
+
stop_scanner() — graceful shutdown
|
|
8
|
+
status() — current scanner health (last tick, queue depth, etc.)
|
|
9
|
+
validate_cron(...) — server-authoritative cron expression validator
|
|
10
|
+
next_n_fires(...) — preview a cron expression's next N fires
|
|
11
|
+
compute_next_due_at(...) — compute the next fire time for any trigger config
|
|
12
|
+
|
|
13
|
+
HTTP API (optional, requires matrx-connect + fastapi):
|
|
14
|
+
matrx_scheduler.api.include_routers(app, prefix="/scheduler")
|
|
15
|
+
|
|
16
|
+
Models (Pydantic):
|
|
17
|
+
SchTask, SchAgentTask, SchTrigger, SchRun
|
|
18
|
+
HydratedTask, AgentRunInput, AgentRunResult
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from ._ext import configure, has_ext, is_configured
|
|
24
|
+
from .cron_helpers import (
|
|
25
|
+
fire_count_in_window,
|
|
26
|
+
next_n_fires,
|
|
27
|
+
validate_cron,
|
|
28
|
+
)
|
|
29
|
+
from .models import (
|
|
30
|
+
AgentRunInput,
|
|
31
|
+
AgentRunResult,
|
|
32
|
+
AuthMode,
|
|
33
|
+
HydratedTask,
|
|
34
|
+
RunStatus,
|
|
35
|
+
SchAgentTask,
|
|
36
|
+
SchRun,
|
|
37
|
+
SchTask,
|
|
38
|
+
SchTrigger,
|
|
39
|
+
ToolRunInput,
|
|
40
|
+
TriggerType,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__version__ = "0.3.0"
|
|
45
|
+
from .next_due import compute_next_due_at
|
|
46
|
+
from .scanner import (
|
|
47
|
+
ScannerStatus,
|
|
48
|
+
is_running,
|
|
49
|
+
start_scanner,
|
|
50
|
+
status,
|
|
51
|
+
stop_scanner,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
# configuration
|
|
57
|
+
"configure",
|
|
58
|
+
"is_configured",
|
|
59
|
+
"has_ext",
|
|
60
|
+
# scanner
|
|
61
|
+
"start_scanner",
|
|
62
|
+
"stop_scanner",
|
|
63
|
+
"status",
|
|
64
|
+
"is_running",
|
|
65
|
+
"ScannerStatus",
|
|
66
|
+
# cron helpers
|
|
67
|
+
"validate_cron",
|
|
68
|
+
"next_n_fires",
|
|
69
|
+
"fire_count_in_window",
|
|
70
|
+
"compute_next_due_at",
|
|
71
|
+
# models
|
|
72
|
+
"SchTask",
|
|
73
|
+
"SchAgentTask",
|
|
74
|
+
"SchTrigger",
|
|
75
|
+
"SchRun",
|
|
76
|
+
"HydratedTask",
|
|
77
|
+
"AgentRunInput",
|
|
78
|
+
"AgentRunResult",
|
|
79
|
+
"ToolRunInput",
|
|
80
|
+
"TriggerType",
|
|
81
|
+
"RunStatus",
|
|
82
|
+
"AuthMode",
|
|
83
|
+
# version
|
|
84
|
+
"__version__",
|
|
85
|
+
]
|