nightshift-grid 0.1.1__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.
- nightshift_grid-0.1.1/LICENSE +202 -0
- nightshift_grid-0.1.1/PKG-INFO +341 -0
- nightshift_grid-0.1.1/README.md +306 -0
- nightshift_grid-0.1.1/chaos_test.py +700 -0
- nightshift_grid-0.1.1/coordinator.py +1316 -0
- nightshift_grid-0.1.1/dashboard.py +151 -0
- nightshift_grid-0.1.1/demo_submit.py +70 -0
- nightshift_grid-0.1.1/discovery.py +460 -0
- nightshift_grid-0.1.1/graphrag.py +222 -0
- nightshift_grid-0.1.1/grid_auth.py +190 -0
- nightshift_grid-0.1.1/grid_store.py +103 -0
- nightshift_grid-0.1.1/grid_tls.py +218 -0
- nightshift_grid-0.1.1/idle_detection.py +920 -0
- nightshift_grid-0.1.1/ingest.py +355 -0
- nightshift_grid-0.1.1/mcp_server.py +859 -0
- nightshift_grid-0.1.1/nightshift.py +792 -0
- nightshift_grid-0.1.1/nightshift_grid.egg-info/PKG-INFO +341 -0
- nightshift_grid-0.1.1/nightshift_grid.egg-info/SOURCES.txt +44 -0
- nightshift_grid-0.1.1/nightshift_grid.egg-info/dependency_links.txt +1 -0
- nightshift_grid-0.1.1/nightshift_grid.egg-info/entry_points.txt +7 -0
- nightshift_grid-0.1.1/nightshift_grid.egg-info/requires.txt +14 -0
- nightshift_grid-0.1.1/nightshift_grid.egg-info/top_level.txt +17 -0
- nightshift_grid-0.1.1/pyproject.toml +63 -0
- nightshift_grid-0.1.1/qdrant_sink.py +553 -0
- nightshift_grid-0.1.1/reduce_graph.py +756 -0
- nightshift_grid-0.1.1/relay.py +238 -0
- nightshift_grid-0.1.1/setup.cfg +4 -0
- nightshift_grid-0.1.1/tests/test_chaos_report.py +155 -0
- nightshift_grid-0.1.1/tests/test_cli.py +276 -0
- nightshift_grid-0.1.1/tests/test_coordinator.py +182 -0
- nightshift_grid-0.1.1/tests/test_coordinator_autotoken.py +143 -0
- nightshift_grid-0.1.1/tests/test_discovery.py +205 -0
- nightshift_grid-0.1.1/tests/test_durability.py +236 -0
- nightshift_grid-0.1.1/tests/test_edge.py +91 -0
- nightshift_grid-0.1.1/tests/test_guards_hid.py +173 -0
- nightshift_grid-0.1.1/tests/test_guards_pressure.py +234 -0
- nightshift_grid-0.1.1/tests/test_guards_worker.py +284 -0
- nightshift_grid-0.1.1/tests/test_ingest.py +119 -0
- nightshift_grid-0.1.1/tests/test_mcp.py +473 -0
- nightshift_grid-0.1.1/tests/test_power.py +250 -0
- nightshift_grid-0.1.1/tests/test_qdrant_sink.py +297 -0
- nightshift_grid-0.1.1/tests/test_reduce.py +200 -0
- nightshift_grid-0.1.1/tests/test_relay.py +337 -0
- nightshift_grid-0.1.1/tests/test_tls_identity.py +258 -0
- nightshift_grid-0.1.1/tests/test_worker_auth.py +183 -0
- nightshift_grid-0.1.1/worker_daemon.py +873 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nightshift-grid
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Fault-tolerant local AI batch grid that harvests idle laptops and office PCs, and yields to their humans instantly
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://github.com/redd34/nightshift-grid
|
|
7
|
+
Project-URL: Issues, https://github.com/redd34/nightshift-grid/issues
|
|
8
|
+
Keywords: ollama,llm,edge-ai,graphrag,mapreduce,distributed,mcp,local-ai
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: fastapi>=0.115.0
|
|
22
|
+
Requires-Dist: uvicorn[standard]>=0.32.0
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Requires-Dist: psutil>=6.1.0
|
|
25
|
+
Requires-Dist: pydantic>=2.9.0
|
|
26
|
+
Requires-Dist: rich>=13.9.0
|
|
27
|
+
Requires-Dist: cryptography>=42
|
|
28
|
+
Requires-Dist: qdrant-client>=1.12.0
|
|
29
|
+
Requires-Dist: pypdf>=5.0.0
|
|
30
|
+
Requires-Dist: mcp>=2.3
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pyflakes>=3.0; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# Nightshift Grid ⚡️
|
|
37
|
+
|
|
38
|
+
[](https://github.com/redd34/nightshift-grid/actions/workflows/ci.yml)
|
|
39
|
+
[](https://github.com/redd34/nightshift-grid/blob/main/LICENSE)
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
**Turn idle laptops, office PCs and legacy mini-PCs into a zero-CapEx, fault-tolerant local AI batch and GraphRAG cluster.**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv tool install git+https://github.com/redd34/nightshift-grid # or: pipx install git+https://...
|
|
46
|
+
nightshift up --mock --workers 3 # coordinator + 3 workers + live dashboard
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
<sub>A real (mock-inference) run recorded with `scripts/record_demo.py`. Benchmarks so far are from a single Mac; see [Known limitations](#known-limitations-and-roadmap).</sub>
|
|
52
|
+
|
|
53
|
+
Nightshift Grid is a lightweight distributed compute engine ("SETI@home meets local enterprise
|
|
54
|
+
MapReduce"). It harvests idle cycles across mixed local machines without slowing down the
|
|
55
|
+
people using them.
|
|
56
|
+
|
|
57
|
+
It doesn't slice one giant model across slow LAN links, where Wi-Fi jitter destroys token
|
|
58
|
+
throughput and one closed laptop lid kills the forward pass. Instead, every machine is a
|
|
59
|
+
**preemptible, fenced micro-worker** running its own quantized small model (`qwen3:4b`,
|
|
60
|
+
`qwen2.5:3b`, `phi4-mini`) via Ollama. A central coordinator hands out **leased** chunks.
|
|
61
|
+
When a human touches the machine, the node yields immediately and its chunk moves to another node.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
submit 24 chunks output_graph.jsonl
|
|
65
|
+
client ──────────────────────▶ Coordinator ───────────────────────▶ (1 record / chunk)
|
|
66
|
+
│ ▲ lease(task_id, lease_id) / heartbeat 3s / complete / preempt
|
|
67
|
+
┌───────────────┼──┼────────────────┐
|
|
68
|
+
▼ ▼ │ ▼
|
|
69
|
+
node-office-pc node-legacy-laptop node-mac-studio each: worker_daemon.py
|
|
70
|
+
(Tier 2) (Tier 1) (Tier 3) + local Ollama
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Why this exists
|
|
76
|
+
|
|
77
|
+
Most teams have gigabytes to terabytes of unstructured PDFs, logs and tickets that can't go to
|
|
78
|
+
cloud APIs for cost or privacy reasons. Meanwhile the office PCs, laptops and mini-PCs that could
|
|
79
|
+
process them sit mostly idle all day and fully idle overnight.
|
|
80
|
+
|
|
81
|
+
The common ways to pool local machines leave a gap for batch pipelines:
|
|
82
|
+
|
|
83
|
+
| Approach | Typical failure mode for batch work | Nightshift Grid |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| **Tensor/layer splitting** (one big model sharded across machines) | Bound by LAN serialization. One node dropping mid-pass stalls the whole request. | Each node runs a whole small model on independent chunks. A dropped node costs one chunk retry. |
|
|
86
|
+
| **Stateless request routing** (load-balance chat requests across nodes) | No durable job state, so a node dying mid-request fails that request. | Stateful MapReduce queue with fenced leases. Every chunk completes or is explicitly dead-lettered. |
|
|
87
|
+
| **Dedicated inference pools** | Machines must be handed over to the cluster. | Good-citizen harvesting: the human always wins and the grid backs off in milliseconds. |
|
|
88
|
+
|
|
89
|
+
Capability tiers decide what each node gets: Tier 1 (<8GB free) does embeddings and chunking,
|
|
90
|
+
Tier 2 (8–24GB) does 3B–8B INT4 entity/relationship extraction, and Tier 3 (>24GB, or Apple
|
|
91
|
+
Silicon with ≥16GB free) does heavy verification and synthesis.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Key highlights and live benchmarks
|
|
96
|
+
|
|
97
|
+
### 1. Sub-250ms GPU yield, including during model load and prompt eval
|
|
98
|
+
|
|
99
|
+
A common trap when wrapping Ollama or `llama.cpp` in Python is breaking out of the token loop
|
|
100
|
+
without closing the underlying socket. The server keeps generating, and the GPU stays pegged
|
|
101
|
+
until the response finishes. A second trap is checking for user activity only when a token
|
|
102
|
+
arrives. That leaves a blind spot while the model is loading weights or evaluating a long prompt.
|
|
103
|
+
|
|
104
|
+
Nightshift runs inference in its own asyncio task. An out-of-band activity watcher polls the
|
|
105
|
+
chaos trigger file every 100ms and OS HID idle time every 500ms: `ioreg -c IOHIDSystem` on macOS,
|
|
106
|
+
`GetLastInputInfo` on Windows, `xprintidle` on Linux. On activity it calls `task.cancel()`,
|
|
107
|
+
which unwinds `async with client.stream(...)` and closes the connection, so Ollama stops
|
|
108
|
+
generating.
|
|
109
|
+
|
|
110
|
+
Measured with `qwen3:4b` on Apple Silicon (GPU utilisation from IOKit, 250ms samples):
|
|
111
|
+
|
|
112
|
+
| Worker cancellation method | GPU utilisation afterwards |
|
|
113
|
+
|---|---|
|
|
114
|
+
| Stop the read loop, leave the response open | **84–94% until the model finishes** |
|
|
115
|
+
| Error raised inside `async with client.stream` | **0% by the first sample**; next request ready in 0.04s |
|
|
116
|
+
| Out-of-band `task.cancel()` (also covers model load and prompt eval) | **0% by the first sample** |
|
|
117
|
+
|
|
118
|
+
### 2. End-to-end chaos benchmarks
|
|
119
|
+
|
|
120
|
+
Three workers (`qwen3:4b`, about 130 tok/s decode) processed a 24-chunk industrial-maintenance
|
|
121
|
+
GraphRAG extraction job per phase, with auth enabled (`python nightshift.py chaos-test --model qwen3:4b`). All three workers ran on one Mac sharing one Ollama
|
|
122
|
+
instance. Multi-machine runs are still to do.
|
|
123
|
+
|
|
124
|
+
| Chaos scenario / metric | Measured result |
|
|
125
|
+
|---|---|
|
|
126
|
+
| **Live human preemption** (user returns mid-job on `node-legacy-laptop`) | **24/24 chunks** in 47.3s, 0 failures |
|
|
127
|
+
| Activity → coordinator reclaims the lease (trigger file, 100ms poll) | **0.01–0.10s** across runs (0.011s in the final run) |
|
|
128
|
+
| Lease reclaimed → re-assigned to another node | **0.8s** |
|
|
129
|
+
| Activity → preempted chunk finished on the backup node | **5.2s** |
|
|
130
|
+
| Work sent to the paused node while the human is active | **0 chunks** |
|
|
131
|
+
| **Worker crash** (`SIGKILL node-office-pc` mid-chunk) | **24/24 chunks** in 45.8s |
|
|
132
|
+
| `SIGKILL` → lease expiry (12s TTL) → re-assigned → done | **11.9s → +1.1s → 16.8s total** |
|
|
133
|
+
| **Coordinator crash** (`SIGKILL` coordinator mid-job, 3s outage, restart on same SQLite file) | **24/24 chunks** in 46.4s, **24 attempts for 24 chunks (0 wasted)** |
|
|
134
|
+
| Restart → ready; in-flight leases finished by their original holders | **0.23s; 2/2** |
|
|
135
|
+
| **Host CPU spike** (busy-loop on all 16 cores mid-job, `--max-host-cpu-pct 65`) | **24/24 chunks** in 54.4s; all 3 workers yielded (`cpu_busy`) in **1.9–2.7s**, 0 leases while loaded, all resumed **2.1s** after the load stopped |
|
|
136
|
+
| Unintended preemptions from the grid's own Ollama load (phases 1–3) | **0** |
|
|
137
|
+
| **Auth** (anonymous submit / worker token on client API / forged completion) | **401 / 401 / 401** |
|
|
138
|
+
| **Output integrity** (4 jobs) | **96/96 records, 0 missing, 0 duplicates, 434 triples** |
|
|
139
|
+
|
|
140
|
+
With `--native-idle-seconds`, detection latency is bounded by the 500ms HID poll rather than the
|
|
141
|
+
100ms file poll. The HID path is covered by unit tests and a live `ioreg` read. An end-to-end
|
|
142
|
+
benchmark with real keyboard input is still to do, because synthesising HID events needs macOS
|
|
143
|
+
Accessibility permission.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Core guarantees
|
|
148
|
+
|
|
149
|
+
| Guarantee | Mechanism |
|
|
150
|
+
|---|---|
|
|
151
|
+
| **No chunk lost** | Every lease has a 12s TTL, extended by 3s heartbeats, and a reaper re-queues expired leases. Preemption and graceful `SIGTERM` hand leases back immediately. A worker can hold only one lease at a time. |
|
|
152
|
+
| **Fenced leases (no split-brain)** | Each assignment carries a unique `lease_id`. A partitioned or stalled worker can't heartbeat or complete a lease that was re-issued. The heartbeat reply (`lease_valid: false`) tells it to abort the zombie compute. |
|
|
153
|
+
| **Race-free pull and pause** | If a human returns while a pull is in flight, the new lease goes straight back. Preempt messages name their lease, so a late one can't revoke a newer lease. A paused node gets no work until it reports idle. |
|
|
154
|
+
| **Dead-letter protection** | Errors and lease expiries count toward `MAX_FAILURES=3`, after which the chunk is parked as `FAILED`. Human preemptions don't count as failures. |
|
|
155
|
+
| **Clean graph output** | Ollama structured outputs (a JSON schema) constrain decoding. `normalize_extraction()` coerces the output to `[subject, RELATION, object]` and canonicalises relation names (`exceeded during` → `EXCEEDED_DURING`) so edges merge across chunks. Reasoning tokens are disabled on thinking models (`qwen3`). Invalid output is a retryable failure. |
|
|
156
|
+
| **Idempotent sink** | `output_graph.jsonl` is fsynced per record and deduplicated by `task_id`, including across a coordinator restart or a failed fsync. Retried completions are no-ops. |
|
|
157
|
+
| **Capability-aware scheduling** | A worker serves the highest tier it's capable of first, so Tier 3 nodes aren't spent on Tier 1 work while Tier 3 work waits. Recovered chunks go to the front of the queue. |
|
|
158
|
+
| **Survives coordinator crashes** | Every state transition is journaled to SQLite (WAL) in one transaction. On restart, queue order, results and dead letters are recovered, and in-flight leases get a fresh TTL so workers that kept computing can still complete them. Workers retry completions for 30s to ride out the restart. About 11k durable transitions/s; a 50k-task job recovers in 0.3s. |
|
|
159
|
+
| **Least-privilege auth** | Separate bearer tokens for workers (`/worker/*`) and clients (`/v1/*`), so a compromised laptop can lease work but can't submit jobs or read results. Each worker registration gets a session secret (`X-Nightshift-Session`), so a live worker's id can't be hijacked. Constant-time comparison. Tokens come from files or env, never argv. Reused token files must be regular, owned by you and 0600 (no symlinks). The coordinator binds loopback by default and refuses a LAN bind without tokens. Browser `Origin` requests are refused, and in no-auth loopback mode untrusted `Host` headers are refused too (DNS-rebinding protection). |
|
|
160
|
+
| **Good citizen beyond the keyboard** | Workers also pause on battery power (`--allow-battery` to opt out) and under thermal throttling (macOS `pmset`, Linux sensors). They pause when *other* processes push host CPU over `--max-host-cpu-pct` (default 65; Ollama and co-located workers are excluded; sustain and hysteresis prevent flapping). They pause when free RAM drops under `--min-free-ram-gb` (default 2.0), evicting the model with `keep_alive: 0`; the resume threshold includes the model's size, so unloading can't cause a pause/reload loop. |
|
|
161
|
+
| **Fail-closed idle sensing** | With `--native-idle-seconds`, a worker that can't read HID idle time refuses to start (`--allow-no-idle-sensor` overrides). Sensors: macOS `ioreg`, Windows `GetLastInputInfo`, GNOME/Wayland Mutter and freedesktop ScreenSaver over D-Bus, X11 `xprintidle`. If a sensor dies at runtime, the worker pauses (`idle_sensor_lost`) rather than guessing. |
|
|
162
|
+
| **Zero-config LAN discovery** | `worker_daemon.py --discover` finds the coordinator over UDP broadcast. The reply carries an HMAC proof keyed by the worker token, so a rogue host can't impersonate the coordinator and harvest tokens. Replays fail (fresh nonce), two valid coordinators are an error, and responses are size-capped and rate-limited (not an amplifier). |
|
|
163
|
+
| **Live vector index (optional)** | `--qdrant-url` adds a Qdrant sink alongside the always-on JSONL. Writes are queued off the event loop, batched, embedded (nomic-embed-text, or a deterministic hash embedder) and upserted idempotently into `nightshift_graph`. Failures spool to disk and replay, and never fail a completion. One embedder per collection is enforced. |
|
|
164
|
+
| **Preemption without lost work (Baton relay, opt-in)** | With `coordinator.py --relay`, a worker streams its partial output to the coordinator as small deltas on the heartbeat it already sends, plus the exact text at the moment it yields. When a lease ends early (human returns, Wi-Fi drops, SIGKILL), the next worker on the **same model** continues the generation from that text instead of from token zero. Checkpoints are fenced by `lease_id`, only grow, and are journaled with the task, so they survive a coordinator crash too. A resumed attempt that fails validation drops the checkpoint, so a bad prefix can't loop. Live resume uses a raw prompt that's verified per model at worker startup (ChatML/Qwen today); other models restart as before. Decode accounting is always on: tokens generated, tokens in results, tokens wasted per node and goodput appear in `/v1/grid/status` → `relay`.
|
|
165
|
+
|
|
166
|
+
**Live fidelity (qwen3:4b, 3 chunks × 25/50/75% cuts, 2 runs):**
|
|
167
|
+
- 18/18 resumed outputs are valid; 6/18 are identical to a fresh run.
|
|
168
|
+
- Mean triple overlap with the fresh run is 0.70, against 0.89 between two fresh runs.
|
|
169
|
+
- So resumed extractions drift more than run-to-run noise, mainly because an ungrammared continuation sometimes closes the object early.
|
|
170
|
+
- **Relay therefore stays opt-in.** It suits slow nodes and long outputs, where restarting costs most. |
|
|
171
|
+
| **Resilient workers** | Non-JSON or 5xx responses are retried, not fatal. If the activity watcher or heartbeat task dies, the worker exits loudly instead of running unprotected. |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Quickstart
|
|
176
|
+
|
|
177
|
+
### 1. Install
|
|
178
|
+
|
|
179
|
+
As a tool (isolated environment; puts `nightshift`, `nightshift-coordinator`, `nightshift-worker`, `nightshift-mcp`,
|
|
180
|
+
`nightshift-dashboard` and `nightshift-credential` on your PATH):
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
uv tool install git+https://github.com/redd34/nightshift-grid # or: pipx install git+https://github.com/redd34/nightshift-grid
|
|
184
|
+
ollama pull qwen3:4b # any small chat model works; pass it with --model
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
From a checkout (for development; every command below also works as `python <script>.py`):
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
git clone https://github.com/redd34/nightshift-grid && cd nightshift-grid
|
|
191
|
+
python3 -m venv .venv && source .venv/bin/activate # or: uv venv .venv
|
|
192
|
+
pip install -e ".[dev]"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 2. Try it in 30 seconds
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
nightshift up --mock --workers 3 # coordinator + 3 workers + live dashboard, one terminal
|
|
199
|
+
nightshift submit --wait # (second terminal) 24-chunk demo job, zero config
|
|
200
|
+
nightshift status # one-shot table; `down` stops the grid and cleans up a crashed session
|
|
201
|
+
nightshift up --model qwen3:4b # same, with real inference
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`up` keeps tokens (0600), per-worker credentials, the state DB, output and logs in a state directory (`.nightshift/`
|
|
205
|
+
in a checkout, `~/.nightshift` for an installed tool, or `$NIGHTSHIFT_STATE_DIR`), so `submit`/`status` need no flags.
|
|
206
|
+
|
|
207
|
+
### 3. Verify
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
python -m pytest -q tests # 379 tests: state machine, durability fuzz, guards, auth, TLS, relay, MCP...
|
|
211
|
+
python nightshift.py chaos-test --mock # 4-phase chaos benchmark, Markdown table to stdout (~2.5 min)
|
|
212
|
+
python nightshift.py chaos-test --model qwen3:4b # same, against live Ollama
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 4. Run a grid by hand
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
python coordinator.py --output output_graph.jsonl --db grid_state.db # loopback, no auth: local dev
|
|
219
|
+
python worker_daemon.py --id node-office-pc --tier 2 --model qwen3:4b
|
|
220
|
+
python worker_daemon.py --id node-legacy-laptop --tier 1 --model qwen3:4b --native-idle-seconds 60
|
|
221
|
+
python worker_daemon.py --id node-mac-studio --tier 3 --model qwen3:4b
|
|
222
|
+
python dashboard.py # live rich TUI
|
|
223
|
+
python demo_submit.py --wait # submit 24 chunks and wait for the summary
|
|
224
|
+
touch /tmp/preempt_node-legacy-laptop # simulate a human; rm the file to release
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 5. From documents to an answerable graph (map → reduce → query)
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
python ingest.py docs/ --tier 2 --wait # chunk .txt/.md/.log/.csv/.jsonl/.pdf → grid job(s) + manifest
|
|
231
|
+
python reduce_graph.py output_graph.jsonl --out graph.json --graphml graph.graphml \
|
|
232
|
+
--qdrant http://localhost:6333 --embed-model nomic-embed-text # entity resolution + nightshift_* collections
|
|
233
|
+
python reduce_graph.py query graph.json "which bearing assemblies were replaced?" --qdrant http://localhost:6333
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`ingest.py` keeps provenance in a sidecar `<job>.manifest.jsonl`, not in the prompt, so workers never see file paths.
|
|
237
|
+
`reduce_graph.py` merges surface variants such as `Compressor turbine`/`Compressor: turbine`, but **never** merges names
|
|
238
|
+
whose numbers differ (`B-101` ≠ `B-102`, `4.2` ≠ `4.5 mm/s`). It only ever writes Qdrant collections prefixed `nightshift_`,
|
|
239
|
+
and it never deletes or recreates a collection.
|
|
240
|
+
|
|
241
|
+
### 6. Drive it from an AI IDE (MCP)
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
claude mcp add nightshift --env NIGHTSHIFT_URL=http://127.0.0.1:8000 --env NIGHTSHIFT_MCP_ROOTS="$PWD" \
|
|
245
|
+
-- "$PWD/.venv/bin/python" "$PWD/mcp_server.py"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The server exposes 4 tools: `grid_status`, `dispatch_mapreduce_text`, `ingest_directory_to_graph` and `query_local_graph`.
|
|
249
|
+
File access is limited to `NIGHTSHIFT_MCP_ROOTS` (symlinks resolved, secret files refused), and tools warn when data
|
|
250
|
+
would leave the machine. See `docs/MCP.md` for Claude Desktop config and the security model.
|
|
251
|
+
|
|
252
|
+
### 7. Deploy as services
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
deploy/gen_token.sh --env > coordinator.env # GRID_WORKER_TOKEN / GRID_CLIENT_TOKEN
|
|
256
|
+
# Coordinator (Linux): hardened systemd unit, steps in deploy/README.md §2
|
|
257
|
+
# Worker on macOS (LaunchAgent) / Linux (systemd --user); preview first with --dry-run:
|
|
258
|
+
printf '%s\n' "$GRID_WORKER_TOKEN" | deploy/install_worker.sh \
|
|
259
|
+
--coordinator http://10.0.0.5:8000 --model qwen3:4b --tier 2 --token-stdin
|
|
260
|
+
# Windows (at-logon task; add -WhatIf to preview):
|
|
261
|
+
# deploy\windows\install_worker.ps1 -Coordinator http://10.0.0.5:8000 -TokenFile worker.token
|
|
262
|
+
# Remove: deploy/install_worker.sh --uninstall [--purge] · Ops guide: deploy/README.md
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Workers run as **per-user** services (a LaunchAgent, systemd `--user`, or an at-logon task), never as system daemons.
|
|
266
|
+
Only a per-user service can see the logged-in user's keyboard and mouse, which preemption depends on.
|
|
267
|
+
|
|
268
|
+
Useful worker flags:
|
|
269
|
+
- `--mock` simulates the token stream.
|
|
270
|
+
- `--tier N` overrides hardware fingerprinting.
|
|
271
|
+
- `--native-idle-seconds N` pauses the worker while HID idle time is under N seconds.
|
|
272
|
+
- `--unload-on-preempt auto|always|never` sends `keep_alive: 0` to free the model's RAM. `auto` only does this under memory pressure.
|
|
273
|
+
- `--allow-battery`, `--min-battery-percent P`, `--ignore-thermal` and `--max-temp-c C` control the power and thermal guards.
|
|
274
|
+
- `--num-ctx N` (default 4096, env `NIGHTSHIFT_NUM_CTX`) is sent with every Ollama request. It is never left unset: Ollama would otherwise load the model at its own default context, which for qwen3 is 262k tokens (~40 GB).
|
|
275
|
+
- `--max-host-cpu-pct P` (default 65, 0 = off) and `--cpu-busy-samples N` control the CPU guard. `--min-free-ram-gb G` (default 2.0) controls the RAM guard.
|
|
276
|
+
- `--allow-no-idle-sensor` keeps the worker running when no HID idle sensor is available.
|
|
277
|
+
- `--discover` (with `--discovery-port`) finds the coordinator on the LAN, and learns its TLS fingerprint through the authenticated reply.
|
|
278
|
+
- `--token-file PATH` sets the worker credential. Otherwise it comes from `$GRID_WORKER_TOKEN`, `$GRID_TOKEN`, `$NIGHTSHIFT_TOKEN_FILE` or `$NIGHTSHIFT_TOKEN`. With a per-worker credential, `--id` is optional, because the credential names its id.
|
|
279
|
+
- `--tls-fingerprint sha256:…` pins the coordinator's certificate. `--ca-file PEM` verifies it against a CA instead. The env equivalents are `$NIGHTSHIFT_TLS_FINGERPRINT` and `$NIGHTSHIFT_CA_FILE`, which the dashboard, `demo_submit.py`, the CLI and the MCP server also read.
|
|
280
|
+
|
|
281
|
+
Coordinator flags:
|
|
282
|
+
- `--host` defaults to `127.0.0.1`.
|
|
283
|
+
- `--db grid_state.db` sets the state file (`none` = in-memory).
|
|
284
|
+
- `--output` sets the JSONL sink.
|
|
285
|
+
- `--worker-token-file` and `--client-token-file` set the tokens. Otherwise they come from `$GRID_WORKER_TOKEN`/`$GRID_CLIENT_TOKEN`, or `$GRID_TOKEN` for both.
|
|
286
|
+
- `--auto-tls` serves HTTPS with a self-signed certificate. It's minted on the first run next to the db and reused afterwards, and its fingerprint is printed at startup. Alternatively, use `--tls-cert` / `--tls-key`. A non-loopback bind refuses plain HTTP unless you pass `--allow-plaintext`, which is for networks that are already encrypted, such as Tailscale/WireGuard.
|
|
287
|
+
- `--worker-auth auto|per-worker|shared` sets how workers authenticate. The default, `auto`, means `per-worker` on a LAN bind: each worker presents a credential bound to its own id, minted with `python grid_auth.py issue --id node-x --worker-token-file worker.token --out node-x.credential`. The master worker token then never leaves the coordinator host.
|
|
288
|
+
- `--allow-insecure` permits a LAN bind without tokens or TLS.
|
|
289
|
+
- `--auto-token` generates and persists 0600 tokens on the first run.
|
|
290
|
+
- `--advertise` / `--no-advertise` and `--discovery-port` control LAN discovery. It's on by default for non-loopback binds.
|
|
291
|
+
- `--qdrant-url URL` or `--qdrant-memory`, plus `--qdrant-collection nightshift_graph` and `--qdrant-embedder auto|ollama|hash`, enable the live vector sink. `python qdrant_sink.py backfill` re-indexes from the JSONL.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## API
|
|
296
|
+
|
|
297
|
+
With tokens configured, `/worker/*` requires `Authorization: Bearer <worker credential>` and `/v1/*` requires the client token. A per-worker credential is `nsw1.<id>.<HMAC(master, id)>`. It only works for its own `worker_id`: using it for any other id gets a 403, and it can't mint new ids.
|
|
298
|
+
|
|
299
|
+
| Endpoint | Purpose |
|
|
300
|
+
|---|---|
|
|
301
|
+
| `GET /healthz` | Liveness probe (unauthenticated, reveals nothing). |
|
|
302
|
+
| `POST /worker/register` | Join, or re-join. A re-join re-queues the node's old leases. |
|
|
303
|
+
| `POST /worker/pull-task?worker_id=` | Lease the next chunk → `{task_id, lease_id, prompt}`. |
|
|
304
|
+
| `POST /worker/heartbeat` | Extend the lease and report telemetry plus progress (`gen_tokens`, and with relay a checkpoint delta `ckpt_from`/`ckpt_delta`) → `{lease_valid, ckpt_len}`. |
|
|
305
|
+
| `POST /worker/preempt` | `{worker_id, reason, detected_at, lease_id?, gen_tokens?, ckpt_from?, ckpt_delta?}`: hand back the lease now, with the final checkpoint. With relay, the next pull of that chunk carries `resume: {text, tokens}`. |
|
|
306
|
+
| `POST /worker/complete` / `/worker/fail` | Fenced by `lease_id`. |
|
|
307
|
+
| `POST /v1/grid/mapreduce` | Submit `{job_name, chunks[], required_tier}`. |
|
|
308
|
+
| `GET /v1/grid/jobs/{id}?results=true` | Progress, attempts, preemptions, dead letters and results. |
|
|
309
|
+
| `GET /v1/grid/status`, `/v1/grid/events?since_seq=` | Dashboard snapshot and structured event log. Sequence numbers keep increasing across restarts. |
|
|
310
|
+
|
|
311
|
+
## Project layout
|
|
312
|
+
|
|
313
|
+
| File | Role |
|
|
314
|
+
|---|---|
|
|
315
|
+
| `coordinator.py` | FastAPI control plane. `GridState` holds the whole lease state machine and takes an injectable clock, with index-backed O(leases) hot paths. |
|
|
316
|
+
| `grid_store.py` | SQLite (WAL) journal: one transaction per state transition, plus recovery. |
|
|
317
|
+
| `grid_auth.py` | Token resolution (file or env), bearer headers, and fail-fast 401 handling. |
|
|
318
|
+
| `worker_daemon.py` | Preemptible async worker: hardware fingerprinting, cancellable Ollama streaming, heartbeats. |
|
|
319
|
+
| `idle_detection.py` | `ActivityMonitor` with pluggable sources: trigger file, HID idle (macOS/Windows/Linux), battery, thermal, foreign CPU load. |
|
|
320
|
+
| `ingest.py` | Map-side input: walks files, chunks on paragraph/sentence boundaries with overlap, dedupes, submits jobs, writes a provenance manifest. |
|
|
321
|
+
| `reduce_graph.py` | Reduce side: entity resolution (key normalization, guarded embedding merge, union-find), graph.json/GraphML, Qdrant sink, and `query`. |
|
|
322
|
+
| `nightshift.py` | One-command CLI: `up` / `down` / `submit` / `status` / `chaos-test` / `token`. |
|
|
323
|
+
| `mcp_server.py` | MCP server (stdio) exposing the grid to Claude Code, Claude Desktop, Cursor and Kiro. See `docs/MCP.md`. |
|
|
324
|
+
| `discovery.py` | UDP LAN discovery with an HMAC proof of token possession. |
|
|
325
|
+
| `qdrant_sink.py` | Live Qdrant sink: background batching, disk spool, embedder consistency, and `backfill`. |
|
|
326
|
+
| `deploy/` | Service installers and templates: macOS LaunchAgent, systemd, Windows Task Scheduler, plus a token generator. See `deploy/README.md`. |
|
|
327
|
+
| `graphrag.py` | Extraction prompt, JSON schema, normalizer and idempotent JSONL sink. |
|
|
328
|
+
| `relay.py` | Baton token relay helpers: JSON progress scanner and online validator, verified ChatML raw-prompt renderer, grammar-per-item structured resume. |
|
|
329
|
+
| `chaos_test.py` | E2E harness with auth on: human-preemption, worker-SIGKILL and coordinator-SIGKILL phases, with zero-loss and zero-duplicate checks. |
|
|
330
|
+
| `dashboard.py`, `demo_submit.py` | TUI and batch submitter. |
|
|
331
|
+
|
|
332
|
+
## Known limitations and roadmap
|
|
333
|
+
|
|
334
|
+
- **Single-host benchmarks so far.** All three workers shared one Mac and one Ollama. The next milestone is a multi-machine pilot. Latency metrics compare worker and coordinator wall clocks, which is exact on one host but subject to NTP skew across hosts.
|
|
335
|
+
- **Transport and identity.** A LAN coordinator serves TLS (`--auto-tls` or your own certificate), and workers pin its fingerprint. Each worker holds a credential that's valid only for its own id, and every call is also bound to the current registration's session. What remains: a compromised worker can still submit bad results *for chunks it leases* (results aren't cross-checked yet), credentials are revoked only by rotating the master worker token, and the self-signed certificate's private key is only as safe as the coordinator host.
|
|
336
|
+
- **Single coordinator.** It's durable, but not highly available. Finished jobs are never pruned, so the state DB grows with history.
|
|
337
|
+
- **Mixed failure policy.** HID idle sensing fails closed. Battery, thermal, CPU and RAM sensors fail open with one warning each.
|
|
338
|
+
- **Qdrant sink and SIGKILL.** Records still in the sink's in-memory queue when the coordinator is SIGKILLed (under a second's worth) are in the JSONL but not in Qdrant until `python qdrant_sink.py backfill` runs.
|
|
339
|
+
- **Noisy hosts.** The default 65% CPU guard pauses workers on a busy developer machine. That's intended, but tune `--max-host-cpu-pct` for build servers.
|
|
340
|
+
- **Not validated on real hardware:** thermal throttling and battery unplugging (unit-tested with fakes only), the Linux and Windows installers (rendered and inspected, but never run on those OSes), and `systemd-analyze`, `shellcheck` and `pwsh` linting.
|
|
341
|
+
- **Extraction quality** from 3–4B models is unmeasured. Next: a small labelled set, plus Tier 3 verification/consensus on a sample.
|