zettelkasten.commit¶
zettelkasten.commit ¶
Debounced, path-scoped git commits for .zettelkasten/ files.
Literature-review manifests, notes, and other .zettelkasten/ files represent
weeks of authoring effort, so writes must be version-controlled and crash-safe.
This module owns a debounced committer that batches scheduled file paths and
commits ONLY those paths — the user's unrelated working-tree changes are never
swept in. It mirrors memory.commit.MemoryCommitter with two deliberate
differences:
- a distinct git author (
zettelkasten-mcp) so its commits are distinguishable frommemory-mcpand user commits, and - no auto-push — local version history is enough for the zettelkasten; the memory subsystem's auto-push-safe rule is not replicated here.
The committer discovers the git repo from the scheduled file path (so it commits
into whichever repo contains .zettelkasten/) and is a no-op when the path is
not inside a git repo — it never raises into the caller.
ZettelCommitter ¶
Batch .zettelkasten/ file commits behind a debounce timer.
Files handed to :meth:schedule are collected and committed together once
the debounce window elapses without further activity. Commits are isolated
(they add only the requested paths, never the user's other staged
changes). Unlike the memory committer, this never pushes.
Source code in zettelkasten/commit.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 | |
schedule ¶
Add a file to the pending commit set and reset the debounce timer.
A fresh schedule is a new authoring event, so it restores the auto-retry
budget: without resetting _consecutive_failures here, once a batch
hits :data:MAX_COMMIT_RETRIES the internal retry path stops re-arming
the timer and every later commit attempt starts already over the cap,
so transient failures would never be retried again. Resetting the
counter re-arms a full retry budget for the newly scheduled file.
Source code in zettelkasten/commit.py
commit_now ¶
Synchronously commit the given paths, bypassing the debounce queue.
Returns the number of files committed; never raises. A transient git
failure (e.g. index.lock contention with a concurrent git process)
is retried a bounded number of times with a short backoff rather than
silently swallowed, so a momentary lock race doesn't lose the commit.
The retry budget is small and capped (:data:MAX_COMMIT_RETRIES) so the
synchronous caller is never blocked for long.
Source code in zettelkasten/commit.py
flush ¶
Flush any pending debounced commit immediately (e.g. on shutdown).
Source code in zettelkasten/commit.py
review_lock ¶
Return the in-process write lock for review name (lazily created).
This is layer 1 of :func:review_write_lock: it serializes threads WITHIN a
single process. It provides no cross-process protection on its own — for the
full guarantee (threads + separate OS processes) use
:func:review_write_lock, which acquires this lock and then an OS advisory
file lock.
Source code in zettelkasten/commit.py
review_write_lock ¶
review_write_lock(name: str, *, graphs_dir: Path | str | None = None, timeout: float = 10.0) -> Iterator[None]
Two-layer (thread + cross-process) write lock for review name.
Acquires the in-process :func:review_lock first (serializes threads within
this process), then an OS advisory file lock on a per-machine lockfile
(serializes SEPARATE OS processes — the MCP server and dashboard backend).
Both layers are released on exit, in reverse order, even on error.
graphs_dir is the resolved store base the caller writes to (it flows
through to :func:_review_lock_path); pass the SAME base used to resolve the
_reviews/<name>.yaml write so both processes derive one shared lock.
Defaults to GRAPHS_DIR when omitted.
Raises :class:TimeoutError if EITHER layer cannot be acquired within
timeout seconds — the in-process layer is acquired with the same timeout
and, on failure, nothing is left held (the OS layer is never reached). On
platforms without an advisory-lock primitive the OS layer degrades to a
best-effort no-op while the in-process layer still holds.
Usage::
with review_write_lock(name, graphs_dir=base):
data = load_review(name)
... # mutate
write_yaml_atomic(path, data)
Source code in zettelkasten/commit.py
box_write_lock ¶
box_write_lock(graph_name: str, *, graphs_dir: Path | str | None = None, timeout: float = 10.0) -> Iterator[None]
Two-layer (thread + cross-process) write lock for a whole graph box.
Holding this lock serializes every write to graph_name across threads
and separate OS processes (the MCP server and the dashboard backend). It is
built on :func:review_write_lock with a box:: key namespace so a box
named x never collides with a review/table/outline also named x.
graphs_dir is the resolved store base; pass the SAME base used to resolve
the box's writes so all processes derive one shared lock. Raises
:class:TimeoutError if the lock cannot be acquired within timeout.
Source code in zettelkasten/commit.py
boxes_write_lock ¶
boxes_write_lock(graph_names: Iterable[str], *, graphs_dir: Path | str | None = None, timeout: float = 10.0) -> Iterator[None]
Acquire :func:box_write_lock for several boxes at once, deadlock-free.
Box names are de-duplicated, empty names dropped, and the rest acquired in
SORTED order. Consistent ordering is what prevents an ABBA deadlock when two
callers lock the same pair of boxes (e.g. a cross-graph link that touches a
source box and _cross) from opposite directions.