compartment 4.2__tar.gz → 4.4__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.
Files changed (94) hide show
  1. {compartment-4.2/src/compartment.egg-info → compartment-4.4}/PKG-INFO +6 -2
  2. {compartment-4.2 → compartment-4.4}/README.md +5 -1
  3. {compartment-4.2 → compartment-4.4}/pyproject.toml +1 -1
  4. {compartment-4.2 → compartment-4.4}/src/compartment/__init__.py +1 -1
  5. {compartment-4.2 → compartment-4.4}/src/compartment/cli.py +98 -24
  6. {compartment-4.2 → compartment-4.4}/src/compartment/menubar.py +397 -28
  7. {compartment-4.2 → compartment-4.4}/src/compartment/server.py +9 -3
  8. {compartment-4.2 → compartment-4.4}/src/compartment/systray.py +554 -31
  9. {compartment-4.2 → compartment-4.4/src/compartment.egg-info}/PKG-INFO +6 -2
  10. {compartment-4.2 → compartment-4.4}/src/compartment.egg-info/SOURCES.txt +2 -0
  11. {compartment-4.2 → compartment-4.4}/tests/test_attempt_pacing.py +22 -4
  12. {compartment-4.2 → compartment-4.4}/tests/test_linux_panel.py +16 -6
  13. compartment-4.4/tests/test_menu_bar_persistence.py +1001 -0
  14. {compartment-4.2 → compartment-4.4}/tests/test_single_instance.py +7 -1
  15. compartment-4.4/tests/test_stays_running.py +169 -0
  16. {compartment-4.2 → compartment-4.4}/LICENSE +0 -0
  17. {compartment-4.2 → compartment-4.4}/setup.cfg +0 -0
  18. {compartment-4.2 → compartment-4.4}/src/compartment/acl.py +0 -0
  19. {compartment-4.2 → compartment-4.4}/src/compartment/agent_skill.py +0 -0
  20. {compartment-4.2 → compartment-4.4}/src/compartment/audit.py +0 -0
  21. {compartment-4.2 → compartment-4.4}/src/compartment/bench.py +0 -0
  22. {compartment-4.2 → compartment-4.4}/src/compartment/claude_desktop.py +0 -0
  23. {compartment-4.2 → compartment-4.4}/src/compartment/claude_hooks.py +0 -0
  24. {compartment-4.2 → compartment-4.4}/src/compartment/claude_memory.py +0 -0
  25. {compartment-4.2 → compartment-4.4}/src/compartment/clients.py +0 -0
  26. {compartment-4.2 → compartment-4.4}/src/compartment/crypto.py +0 -0
  27. {compartment-4.2 → compartment-4.4}/src/compartment/dash.py +0 -0
  28. {compartment-4.2 → compartment-4.4}/src/compartment/data/agent-skill/SKILL.md +0 -0
  29. {compartment-4.2 → compartment-4.4}/src/compartment/data/app.icns +0 -0
  30. {compartment-4.2 → compartment-4.4}/src/compartment/data/app.png +0 -0
  31. {compartment-4.2 → compartment-4.4}/src/compartment/data/hermes-plugin/__init__.py +0 -0
  32. {compartment-4.2 → compartment-4.4}/src/compartment/data/hermes-plugin/plugin.yaml +0 -0
  33. {compartment-4.2 → compartment-4.4}/src/compartment/data/menubar.png +0 -0
  34. {compartment-4.2 → compartment-4.4}/src/compartment/data/menubar@2x.png +0 -0
  35. {compartment-4.2 → compartment-4.4}/src/compartment/data/starter.mpack +0 -0
  36. {compartment-4.2 → compartment-4.4}/src/compartment/data/tray.ico +0 -0
  37. {compartment-4.2 → compartment-4.4}/src/compartment/embed.py +0 -0
  38. {compartment-4.2 → compartment-4.4}/src/compartment/home.py +0 -0
  39. {compartment-4.2 → compartment-4.4}/src/compartment/longmemeval.py +0 -0
  40. {compartment-4.2 → compartment-4.4}/src/compartment/models/bge-small-en-v1.5-int8/model_quantized.onnx +0 -0
  41. {compartment-4.2 → compartment-4.4}/src/compartment/models/bge-small-en-v1.5-int8/tokenizer.json +0 -0
  42. {compartment-4.2 → compartment-4.4}/src/compartment/offline_guard.py +0 -0
  43. {compartment-4.2 → compartment-4.4}/src/compartment/packs.py +0 -0
  44. {compartment-4.2 → compartment-4.4}/src/compartment/platforms.py +0 -0
  45. {compartment-4.2 → compartment-4.4}/src/compartment/ranking.py +0 -0
  46. {compartment-4.2 → compartment-4.4}/src/compartment/salience.py +0 -0
  47. {compartment-4.2 → compartment-4.4}/src/compartment/selftest.py +0 -0
  48. {compartment-4.2 → compartment-4.4}/src/compartment/session.py +0 -0
  49. {compartment-4.2 → compartment-4.4}/src/compartment/store.py +0 -0
  50. {compartment-4.2 → compartment-4.4}/src/compartment/vault.py +0 -0
  51. {compartment-4.2 → compartment-4.4}/src/compartment/vaultfile.py +0 -0
  52. {compartment-4.2 → compartment-4.4}/src/compartment/vindex.py +0 -0
  53. {compartment-4.2 → compartment-4.4}/src/compartment/wire.py +0 -0
  54. {compartment-4.2 → compartment-4.4}/src/compartment.egg-info/dependency_links.txt +0 -0
  55. {compartment-4.2 → compartment-4.4}/src/compartment.egg-info/entry_points.txt +0 -0
  56. {compartment-4.2 → compartment-4.4}/src/compartment.egg-info/requires.txt +0 -0
  57. {compartment-4.2 → compartment-4.4}/src/compartment.egg-info/top_level.txt +0 -0
  58. {compartment-4.2 → compartment-4.4}/tests/test_acl_audit_packs.py +0 -0
  59. {compartment-4.2 → compartment-4.4}/tests/test_acl_precedence.py +0 -0
  60. {compartment-4.2 → compartment-4.4}/tests/test_agent_skill.py +0 -0
  61. {compartment-4.2 → compartment-4.4}/tests/test_audit_chain.py +0 -0
  62. {compartment-4.2 → compartment-4.4}/tests/test_autoconnect.py +0 -0
  63. {compartment-4.2 → compartment-4.4}/tests/test_claude_desktop.py +0 -0
  64. {compartment-4.2 → compartment-4.4}/tests/test_claude_hooks_and_integrate.py +0 -0
  65. {compartment-4.2 → compartment-4.4}/tests/test_claude_memory_and_recent.py +0 -0
  66. {compartment-4.2 → compartment-4.4}/tests/test_clients.py +0 -0
  67. {compartment-4.2 → compartment-4.4}/tests/test_connect_buttons.py +0 -0
  68. {compartment-4.2 → compartment-4.4}/tests/test_connect_environment.py +0 -0
  69. {compartment-4.2 → compartment-4.4}/tests/test_crypto.py +0 -0
  70. {compartment-4.2 → compartment-4.4}/tests/test_dash.py +0 -0
  71. {compartment-4.2 → compartment-4.4}/tests/test_instructions.py +0 -0
  72. {compartment-4.2 → compartment-4.4}/tests/test_journal_framing.py +0 -0
  73. {compartment-4.2 → compartment-4.4}/tests/test_login_item_identity.py +0 -0
  74. {compartment-4.2 → compartment-4.4}/tests/test_long_memory_recall.py +0 -0
  75. {compartment-4.2 → compartment-4.4}/tests/test_macos_bundle.py +0 -0
  76. {compartment-4.2 → compartment-4.4}/tests/test_menubar.py +0 -0
  77. {compartment-4.2 → compartment-4.4}/tests/test_offline_guard_paths.py +0 -0
  78. {compartment-4.2 → compartment-4.4}/tests/test_offline_selftest_bench.py +0 -0
  79. {compartment-4.2 → compartment-4.4}/tests/test_ondisk_format.py +0 -0
  80. {compartment-4.2 → compartment-4.4}/tests/test_pack_trust.py +0 -0
  81. {compartment-4.2 → compartment-4.4}/tests/test_platforms.py +0 -0
  82. {compartment-4.2 → compartment-4.4}/tests/test_ranking.py +0 -0
  83. {compartment-4.2 → compartment-4.4}/tests/test_relations.py +0 -0
  84. {compartment-4.2 → compartment-4.4}/tests/test_salience.py +0 -0
  85. {compartment-4.2 → compartment-4.4}/tests/test_session_entropy.py +0 -0
  86. {compartment-4.2 → compartment-4.4}/tests/test_session_lock.py +0 -0
  87. {compartment-4.2 → compartment-4.4}/tests/test_starter_data_quality.py +0 -0
  88. {compartment-4.2 → compartment-4.4}/tests/test_starter_packs.py +0 -0
  89. {compartment-4.2 → compartment-4.4}/tests/test_starter_visibility.py +0 -0
  90. {compartment-4.2 → compartment-4.4}/tests/test_status_bar_install.py +0 -0
  91. {compartment-4.2 → compartment-4.4}/tests/test_systray.py +0 -0
  92. {compartment-4.2 → compartment-4.4}/tests/test_tamper.py +0 -0
  93. {compartment-4.2 → compartment-4.4}/tests/test_twofa.py +0 -0
  94. {compartment-4.2 → compartment-4.4}/tests/test_vault_ops.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compartment
3
- Version: 4.2
3
+ Version: 4.4
4
4
  Summary: Compartment - high-security, fully offline, encrypted vector memory for AI agents (MCP)
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/MaxFreedomPollard/Compartment
@@ -50,7 +50,11 @@ Dynamic: license-file
50
50
 
51
51
  # Compartment
52
52
 
53
- ### Superior agentic memory, encrypted at rest.
53
+ [![MCP Toplist](https://mcptoplist.com/badge/io.github.MaxFreedomPollard%2Fcompartment.svg)](https://mcptoplist.com/server/io.github.MaxFreedomPollard%2Fcompartment)
54
+
55
+ <a href="https://glama.ai/mcp/servers/MaxFreedomPollard/Compartment"><img width="380" height="200" src="https://glama.ai/mcp/servers/MaxFreedomPollard/Compartment/badge" alt="Compartment MCP server" /></a>
56
+
57
+ ### Durable agentic memory, encrypted at rest.
54
58
 
55
59
  Your AI agent forgets you the moment the session ends. Compartment ends
56
60
  that. With Compartment, your AI agent gets better with experience: it keeps
@@ -1,6 +1,10 @@
1
1
  # Compartment
2
2
 
3
- ### Superior agentic memory, encrypted at rest.
3
+ [![MCP Toplist](https://mcptoplist.com/badge/io.github.MaxFreedomPollard%2Fcompartment.svg)](https://mcptoplist.com/server/io.github.MaxFreedomPollard%2Fcompartment)
4
+
5
+ <a href="https://glama.ai/mcp/servers/MaxFreedomPollard/Compartment"><img width="380" height="200" src="https://glama.ai/mcp/servers/MaxFreedomPollard/Compartment/badge" alt="Compartment MCP server" /></a>
6
+
7
+ ### Durable agentic memory, encrypted at rest.
4
8
 
5
9
  Your AI agent forgets you the moment the session ends. Compartment ends
6
10
  that. With Compartment, your AI agent gets better with experience: it keeps
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "compartment"
7
- version = "4.2"
7
+ version = "4.4"
8
8
  description = "Compartment - high-security, fully offline, encrypted vector memory for AI agents (MCP)"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -1,6 +1,6 @@
1
1
  """Compartment - high-security, fully offline, encrypted vector memory for AI agents."""
2
2
 
3
- __version__ = "4.2"
3
+ __version__ = "4.4"
4
4
 
5
5
  from . import offline_guard as _og
6
6
 
@@ -252,6 +252,14 @@ def _linux_gui_available() -> bool:
252
252
  return False
253
253
 
254
254
 
255
+ #: Where to tell the user to look, once there is something to look at.
256
+ _APP_IS_AT = {
257
+ "darwin": "look for the icon in your menu bar",
258
+ "win32": "look for the icon in your notification area",
259
+ "": "its window is open, and Compartment is in your applications menu",
260
+ }
261
+
262
+
255
263
  def _start_status_bar_app(vault: str) -> None:
256
264
  """Put the status bar app up now, and again at every login.
257
265
 
@@ -273,14 +281,18 @@ def _start_status_bar_app(vault: str) -> None:
273
281
  print(" The CLI and the MCP server are ready, and `compartment "
274
282
  "dash` opens the vault in a browser.")
275
283
  return
284
+ state = ""
276
285
  try:
277
- state = app.set_login(True)
278
- # On Linux this is the applications menu entry. Nothing starts at
279
- # login there: without a tray icon to sit quietly in, an app that
280
- # started itself would just be a window in your face at every
281
- # sign-in.
282
- print(f" {'applications menu entry' if linux else 'start at login'}"
283
- f": {state}")
286
+ state = app.set_login(True, vault)
287
+ # Linux used to be told about its applications menu entry here,
288
+ # because nothing started at login there. That stopped being true
289
+ # when it got a real autostart entry, and it is further from true
290
+ # now that it gets a systemd user service, so it is reported the
291
+ # same way as everywhere else - and the menu entry, which is a
292
+ # different thing, is named separately.
293
+ print(f" start at login: {state}")
294
+ if linux:
295
+ print(" applications menu entry: on")
284
296
  except Exception as exc: # noqa: BLE001
285
297
  print(f" could not register the app ({exc})")
286
298
  if linux and not _linux_gui_available():
@@ -292,6 +304,24 @@ def _start_status_bar_app(vault: str) -> None:
292
304
  print(" Everything else works now: the CLI, the MCP server, and "
293
305
  "`compartment dash`.")
294
306
  return
307
+ # Let the supervisor start it where there is one, so the copy that comes
308
+ # up is the copy that gets restarted if it dies. On macOS bootstrapping
309
+ # the agent a moment ago already did it (RunAtLoad); on Linux and Windows
310
+ # it is one more call. Starting a second copy after that is what used to
311
+ # put two icons in the menu bar, and the copy started here is the one
312
+ # that dies with the terminal.
313
+ supervised = False
314
+ if state == "on":
315
+ try:
316
+ supervised = (sys.platform == "darwin" or app.start_supervised())
317
+ except Exception: # noqa: BLE001
318
+ supervised = False
319
+ if supervised:
320
+ if _wait_for_status_bar_app():
321
+ print(f" app started - {_APP_IS_AT.get(sys.platform, _APP_IS_AT[''])}")
322
+ return
323
+ print(" start at login is registered, but no icon appeared; "
324
+ "starting one now")
295
325
  try:
296
326
  exe = shutil.which("compartment")
297
327
  argv = ([exe, "--vault", vault, "menubar"] if exe else
@@ -303,20 +333,43 @@ def _start_status_bar_app(vault: str) -> None:
303
333
  kwargs["creationflags"] = 0x00000008 # DETACHED_PROCESS
304
334
  else:
305
335
  kwargs["start_new_session"] = True # outlive this shell
306
- subprocess.Popen(argv, **kwargs)
307
- if sys.platform == "darwin":
308
- print(" app started - look for the icon in your menu bar")
309
- elif sys.platform == "win32":
310
- print(" app started - look for the icon in your notification "
311
- "area")
312
- else:
313
- print(" app started - its window is open, and Compartment is "
314
- "in your applications menu")
336
+ proc = subprocess.Popen(argv, **kwargs)
337
+ # Spawning is not starting. The app exits straight away for several
338
+ # ordinary reasons - PyObjC missing, another copy already holding
339
+ # the lock, a broken interpreter - and every one of them used to end
340
+ # with the install saying "look for the icon in your menu bar" over
341
+ # a menu bar that had nothing new in it. Still running after a
342
+ # couple of seconds is the most an installer can honestly claim.
343
+ try:
344
+ status = proc.wait(timeout=2.0)
345
+ except subprocess.TimeoutExpired:
346
+ status = None
347
+ if status is not None:
348
+ print(f" the app exited immediately (status {status}). "
349
+ "Run `compartment panel` to see what it says.")
350
+ return
351
+ print(f" app started - {_APP_IS_AT.get(sys.platform, _APP_IS_AT[''])}")
315
352
  except Exception as exc: # noqa: BLE001
316
353
  print(f" could not start the app ({exc}); run "
317
354
  "`compartment panel` yourself")
318
355
 
319
356
 
357
+ def _wait_for_status_bar_app(timeout: float = 15.0) -> bool:
358
+ """Did the app actually come up?
359
+
360
+ Asked because "the login item is registered" and "the app is running"
361
+ are different claims, and only the second one is what the user was
362
+ promised.
363
+ """
364
+ app = _tray_app()
365
+ deadline = time.monotonic() + timeout
366
+ while time.monotonic() < deadline:
367
+ if app.running_pids():
368
+ return True
369
+ time.sleep(0.3)
370
+ return False
371
+
372
+
320
373
  def _install_kind() -> tuple[str, list[str]]:
321
374
  """How this copy was installed, and the command that upgrades it."""
322
375
  exe = Path(sys.argv[0]).resolve()
@@ -359,6 +412,21 @@ def cmd_update(args) -> None:
359
412
 
360
413
  def _restart_status_bar_app(vault: str) -> None:
361
414
  """Stop the running status bar app and start the new build."""
415
+ # One supervisor operation rather than kill-then-start. The supervisor
416
+ # relaunches a process that exited non-zero, so the copy this function
417
+ # used to start raced the one launchd or systemd had already brought
418
+ # back, and whichever lost the lock left with exit 0. Since the job is
419
+ # unchanged, the process that comes back runs whatever the upgrade just
420
+ # wrote at the path it names.
421
+ try:
422
+ app = _tray_app()
423
+ restart = (app.restart_agent if sys.platform == "darwin"
424
+ else app.restart_supervised)
425
+ if restart() and _wait_for_status_bar_app():
426
+ print(" app restarted - the supervisor is running the new build")
427
+ return
428
+ except Exception: # noqa: BLE001
429
+ pass
362
430
  try:
363
431
  app = _tray_app()
364
432
  app.quit_running()
@@ -372,14 +440,19 @@ def cmd_uninstall(args) -> None:
372
440
  print(f"Compartment {__version__} - uninstalling")
373
441
  try:
374
442
  app = _tray_app()
443
+ linux = sys.platform not in ("darwin", "win32")
444
+ # Deregister before killing, not after. On macOS the agent has
445
+ # KeepAlive, so a copy stopped while the job is still registered
446
+ # exits non-zero and launchd puts the icon straight back - during an
447
+ # uninstall, from a plist that is about to be deleted.
448
+ print(f" start at login: {app.set_login(False)}")
449
+ if linux:
450
+ print(" applications menu entry: removed")
375
451
  try:
376
- app.quit_running()
377
- print(" app stopped")
452
+ print(" app stopped" if app.quit_running()
453
+ else " app: nothing was running")
378
454
  except Exception: # noqa: BLE001
379
455
  pass
380
- linux = sys.platform not in ("darwin", "win32")
381
- print(f" {'applications menu entry' if linux else 'start at login'}"
382
- f": {app.set_login(False)}")
383
456
  except Exception as exc: # noqa: BLE001
384
457
  print(f" app: nothing to remove ({exc})")
385
458
  if sys.platform == "darwin":
@@ -583,7 +656,7 @@ def cmd_menubar(args) -> None:
583
656
  """The status bar / tray app."""
584
657
  app = _tray_app()
585
658
  if args.login is not None:
586
- print(app.set_login(args.login == "on")
659
+ print(app.set_login(args.login == "on", args.vault)
587
660
  if args.login in ("on", "off") else app.login_status())
588
661
  sys.exit(0)
589
662
  if args.self_check:
@@ -1648,8 +1721,9 @@ def main(argv: list[str] | None = None) -> None:
1648
1721
  p.add_argument("--login", nargs="?", const="status",
1649
1722
  choices=["on", "off", "status"],
1650
1723
  help="start Compartment at login or sign-in (on/off), or "
1651
- "show the state. On Linux this is the applications "
1652
- "menu entry, since there is no icon to start")
1724
+ "show the state. Where the system can supervise it "
1725
+ "(launchd, a systemd user service, a scheduled task) "
1726
+ "the app is also put back if it stops on its own")
1653
1727
  p.set_defaults(fn=cmd_menubar)
1654
1728
 
1655
1729
  ph = sub.add_parser("hook", help="Claude Code capture hook (deterministic "