agent-memory-cli 0.1.2__tar.gz → 0.1.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 (81) hide show
  1. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/CHANGELOG.md +17 -0
  2. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/PKG-INFO +7 -6
  3. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/README.md +6 -5
  4. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/__init__.py +1 -1
  5. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/cli.py +3 -3
  6. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/doctor.py +9 -7
  7. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/startup.py +47 -4
  8. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/sync.py +258 -63
  9. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_doctor.py +19 -15
  10. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_startup.py +100 -0
  11. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_sync.py +359 -13
  12. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/.gitignore +0 -0
  13. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/LICENSE +0 -0
  14. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/Makefile +0 -0
  15. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/pyproject.toml +0 -0
  16. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/scripts/check_installed_wheel.py +0 -0
  17. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/scripts/release_notes.py +0 -0
  18. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/__main__.py +0 -0
  19. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/archive.py +0 -0
  20. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/debrief.py +0 -0
  21. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/events.py +0 -0
  22. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/git_runner.py +0 -0
  23. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/home.py +0 -0
  24. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/layout.py +0 -0
  25. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/org.py +0 -0
  26. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/prerequisites.py +0 -0
  27. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/publication.py +0 -0
  28. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/setup/__init__.py +0 -0
  29. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/setup/claude.py +0 -0
  30. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/setup/codex.py +0 -0
  31. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/setup/common.py +0 -0
  32. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/setup/legacy.py +0 -0
  33. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/status.py +0 -0
  34. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/templates/org-memory/decisions.md +0 -0
  35. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/templates/org-memory/recent.md +0 -0
  36. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/templates/org-memory/rules.md +0 -0
  37. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/templates/project-memory/decision_log.md +0 -0
  38. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/templates/project-memory/known_debt.md +0 -0
  39. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/templates/project-memory/open_threads.md +0 -0
  40. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/templates/project-memory/project_facts.md +0 -0
  41. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/templates/workflow/SKILL.md +0 -0
  42. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/src/agent_memory/workflow.py +0 -0
  43. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/bad_layout/expected.json +0 -0
  44. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/.hidden/2026/08/20260825-alice-abc12345.md +0 -0
  45. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/2026/08/20260825-alice-1f3a9c2b.md +0 -0
  46. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/2026/08/20260825-alice-ABCD.md +0 -0
  47. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/2026/08/20260825_alice_abc12345.md +0 -0
  48. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/2026/09/20260825-alice-def12345.md +0 -0
  49. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/20260825-alice-abc12345.md +0 -0
  50. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/empty_store/expected.json +0 -0
  51. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/empty_store/org-memory/debriefs/.gitkeep +0 -0
  52. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/missing_debriefs_dir/expected.json +0 -0
  53. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/missing_debriefs_dir/org-memory/recent.md +0 -0
  54. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/staging_artifact/expected.json +0 -0
  55. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/staging_artifact/org-memory/debriefs/demo-project/2026/08/.stage.20260825-alice-77xx88yy.md.a1b2c3 +0 -0
  56. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/staging_artifact/org-memory/debriefs/demo-project/2026/08/20260825-alice-1f3a9c2b.md +0 -0
  57. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/valid_store/expected.json +0 -0
  58. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/.gitkeep +0 -0
  59. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/Demo_Project/2026/08/20260825-bob-ops-9f00aa11.md +0 -0
  60. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/demo-project/2026/08/20260825-alice-1f3a9c2b.md +0 -0
  61. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/demo.project/2026/07/20260701-Alice_2.dev-00aa11bb.md +0 -0
  62. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/other-project/2026/12/20261203-bob-9e0d44aa.md +0 -0
  63. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/conftest.py +0 -0
  64. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/fixtures/setup/claude_settings_fleet.json +0 -0
  65. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/fixtures/setup/codex_hooks_kernel.json +0 -0
  66. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/golden/canonical_memory_gitignore.txt +0 -0
  67. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/golden/debrief_record.md +0 -0
  68. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/golden/doctor_memory_0.4.5.json +0 -0
  69. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/golden/doctor_memory_0.4.5.txt +0 -0
  70. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/golden/event_record.md +0 -0
  71. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_archive.py +0 -0
  72. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_cli.py +0 -0
  73. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_debrief.py +0 -0
  74. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_events.py +0 -0
  75. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_home.py +0 -0
  76. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_layout.py +0 -0
  77. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_org.py +0 -0
  78. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_prerequisites.py +0 -0
  79. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_setup.py +0 -0
  80. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_status.py +0 -0
  81. {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.4}/tests/test_workflow.py +0 -0
@@ -4,6 +4,23 @@ All notable changes to agent-memory are documented in this file. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 0.1.4 - 2026-10-26
8
+
9
+ ### Fixed
10
+
11
+ - `pull` and `push` sync one at a time per home and never abort a rebase they did not start ([sync](docs/commands.md#sync)).
12
+ - `pull` with only unpushed commits no longer also reports "already synced" ([sync](docs/commands.md#sync)).
13
+ - `startup --pull` no longer publishes a debrief or event another agent wrote ([startup](docs/commands.md#startup)).
14
+ - `pull` and `push` update past untracked files the incoming commits do not touch, so SessionStart keeps pulling ([sync](docs/commands.md#sync)).
15
+ - `doctor` on Python 3.14 reports a debrief record it cannot inspect as unreadable, not as an irregular file ([doctor](docs/commands.md#doctor)).
16
+ - `enable` starts a new memory repository on `main` even where git's `init.defaultBranch` is `master` ([sync](docs/commands.md#sync)).
17
+
18
+ ## 0.1.3 - 2026-09-29
19
+
20
+ ### Fixed
21
+
22
+ - `pull`, `push`, and SessionStart sync can resolve homes with local memory changes behind upstream ([sync](docs/commands.md#sync)).
23
+
7
24
  ## 0.1.2 - 2026-09-17
8
25
 
9
26
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-memory-cli
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Cross-session memory for coding agents: plain files, git-native, no server
5
5
  Project-URL: Homepage, https://github.com/kiloloop/agent-memory
6
6
  Project-URL: Source, https://github.com/kiloloop/agent-memory
@@ -105,7 +105,7 @@ Python 3.10+ is required; sync needs git 2.25+, hooks use Bash, and
105
105
  agent-memory enable --remote git@github.com:YOUR_ORG/agent-memory-store.git
106
106
  agent-memory push --agent codex
107
107
  # After another machine pushes, with a clean local tree:
108
- agent-memory pull
108
+ agent-memory pull --agent claude
109
109
  ```
110
110
 
111
111
  Elsewhere, `agent-memory clone <remote-url> --home <local-home>`, then repeat
@@ -141,13 +141,14 @@ points into a home's `projects/` tree. Project selection uses `--project` or a
141
141
  matching binding/marker. OACP is not required.
142
142
 
143
143
  Sync makes the home a git repository with an allowlist and `.oacp-memory-repo`
144
- marker. `push` commits selected paths; `pull` fast-forwards a clean tree that
145
- is not ahead or diverged. No merges; keys and setup receipts stay local.
146
- Network verbs time out after 30 seconds.
144
+ marker. `push` and `pull --agent NAME` commit selected paths, rebase when behind,
145
+ and deliver. A plain `pull` updates a clean tree and reports unpushed commits.
146
+ Conflicts abort the rebase and retain local commits. Keys and setup receipts
147
+ stay local. Network and rebase operations time out after 30 seconds.
147
148
 
148
149
  `setup` installs a SessionStart hook and memory workflow. `startup` lists
149
150
  metadata for the four project files, then the three curated org files; it
150
- injects no content. `--pull` refreshes first, warning on failure. The workflow
151
+ injects no content. `--pull` commits and syncs first under the runtime identity, warning on failure. The workflow
151
152
  tells the agent to run `recall` for an 8,000-character bounded read. Raise
152
153
  `--max-chars` or read remaining files directly when cut. `capture` records
153
154
  decisions during work. At the end, update threads and debt, optionally publish
@@ -75,7 +75,7 @@ Python 3.10+ is required; sync needs git 2.25+, hooks use Bash, and
75
75
  agent-memory enable --remote git@github.com:YOUR_ORG/agent-memory-store.git
76
76
  agent-memory push --agent codex
77
77
  # After another machine pushes, with a clean local tree:
78
- agent-memory pull
78
+ agent-memory pull --agent claude
79
79
  ```
80
80
 
81
81
  Elsewhere, `agent-memory clone <remote-url> --home <local-home>`, then repeat
@@ -111,13 +111,14 @@ points into a home's `projects/` tree. Project selection uses `--project` or a
111
111
  matching binding/marker. OACP is not required.
112
112
 
113
113
  Sync makes the home a git repository with an allowlist and `.oacp-memory-repo`
114
- marker. `push` commits selected paths; `pull` fast-forwards a clean tree that
115
- is not ahead or diverged. No merges; keys and setup receipts stay local.
116
- Network verbs time out after 30 seconds.
114
+ marker. `push` and `pull --agent NAME` commit selected paths, rebase when behind,
115
+ and deliver. A plain `pull` updates a clean tree and reports unpushed commits.
116
+ Conflicts abort the rebase and retain local commits. Keys and setup receipts
117
+ stay local. Network and rebase operations time out after 30 seconds.
117
118
 
118
119
  `setup` installs a SessionStart hook and memory workflow. `startup` lists
119
120
  metadata for the four project files, then the three curated org files; it
120
- injects no content. `--pull` refreshes first, warning on failure. The workflow
121
+ injects no content. `--pull` commits and syncs first under the runtime identity, warning on failure. The workflow
121
122
  tells the agent to run `recall` for an 8,000-character bounded read. Raise
122
123
  `--max-chars` or read remaining files directly when cut. `capture` records
123
124
  decisions during work. At the end, update threads and debt, optionally publish
@@ -4,6 +4,6 @@
4
4
 
5
5
  from .home import HomeError, HomeResolution, resolve_home
6
6
 
7
- __version__ = "0.1.2"
7
+ __version__ = "0.1.4"
8
8
 
9
9
  __all__ = ["HomeError", "HomeResolution", "__version__", "resolve_home"]
@@ -92,7 +92,7 @@ def build_parser() -> argparse.ArgumentParser:
92
92
  clone_ = add("clone", _clone, "Clone a memory repository into the home.")
93
93
  clone_.add_argument("url", help="git remote URL to clone")
94
94
  clone_.add_argument("--force", action="store_true", help="move a non-empty home aside before cloning")
95
- add("pull", _pull, "Fast-forward the home from its upstream when the tree is clean and not ahead.")
95
+ add("pull", _pull, "Update from upstream; with --agent, commit allowlisted changes and deliver them too.", agent)
96
96
  add("push", _push, "Commit the allowlisted memory changes and push them when a remote exists.", agent)
97
97
  add("disable", _disable, "Remove the sync marker; the repository stays in place.")
98
98
  for name, handler, help_text, positional, positional_help in (
@@ -121,7 +121,7 @@ def build_parser() -> argparse.ArgumentParser:
121
121
  "startup",
122
122
  _startup,
123
123
  "Print the session-start manifest: the memory files to read, in order, with their readability, size and "
124
- "age, and where the sync stands. Content is never included. With --pull, fast-forward the home first.",
124
+ "age, and where the sync stands. Content is never included. With --pull, commit and sync memory under the runtime identity first.",
125
125
  )
126
126
  startup_.add_argument("--runtime", choices=startup.RUNTIMES, required=True, help="shape the output for this runtime's hook")
127
127
  startup_.add_argument("--project", metavar="ID", help="the project tier to list (default: the one the binding or marker names)")
@@ -297,7 +297,7 @@ def _clone(args: argparse.Namespace) -> int:
297
297
 
298
298
 
299
299
  def _pull(args: argparse.Namespace) -> int:
300
- return _report(sync.pull(resolve_home(args.home).path))
300
+ return _report(sync.pull(resolve_home(args.home).path, agent=args.agent))
301
301
 
302
302
 
303
303
  def _push(args: argparse.Namespace) -> int:
@@ -237,7 +237,7 @@ def check_org_memory(home: Path) -> Category:
237
237
  for name in sorted(dirnames):
238
238
  entry = current / name
239
239
  try:
240
- is_link = entry.is_symlink()
240
+ is_link = stat.S_ISLNK(os.lstat(entry).st_mode)
241
241
  except OSError as exc:
242
242
  walk_errors.append(f"{_relative(entry)}: {exc.__class__.__name__}")
243
243
  continue
@@ -258,16 +258,18 @@ def check_org_memory(home: Path) -> Category:
258
258
  staging.append(rel)
259
259
  continue
260
260
  # The namespace holds regular files reached without following links;
261
- # a classification failure surfaces, never raises.
261
+ # a classification failure surfaces, never raises. lstat, not the
262
+ # pathlib predicates: since Python 3.14 those answer False on any
263
+ # OSError, so a record that could not be inspected would read as irregular.
262
264
  try:
263
- if file_path.is_symlink():
264
- irregular.append(f"{rel} (symlink)")
265
- continue
266
- regular = file_path.is_file()
265
+ mode = os.lstat(file_path).st_mode
267
266
  except OSError as exc:
268
267
  walk_errors.append(f"{rel}: {exc.__class__.__name__}")
269
268
  continue
270
- if not regular:
269
+ if stat.S_ISLNK(mode):
270
+ irregular.append(f"{rel} (symlink)")
271
+ continue
272
+ if not stat.S_ISREG(mode):
271
273
  irregular.append(f"{rel} (not a regular file)")
272
274
  continue
273
275
  total += 1
@@ -14,6 +14,12 @@ files, both from the layout table; ``events/``, ``debriefs/`` and
14
14
  ``archive/`` are excluded), and the rendered text is cut at a character
15
15
  budget with a notice, so a hook can never flood a session.
16
16
 
17
+ With ``--pull`` the session's runtime is the publishing identity, and a
18
+ path another agent wrote is not its to publish: a debrief names its agent in
19
+ its filename and an event in its frontmatter, so either one, when it names
20
+ someone else, is held for that agent's own session. Files that carry no
21
+ author (the curated org files, the project tier) publish as before.
22
+
17
23
  Output shapes: ``--json`` is the manifest itself, ``schema_version`` first;
18
24
  the default is what the runtime's hook expects on stdout: plain text for
19
25
  claude, whose session start takes stdout as context, and the hook JSON
@@ -28,8 +34,9 @@ import stat
28
34
  from pathlib import Path
29
35
  from typing import Any, Dict, List, Optional, Sequence, Tuple
30
36
 
31
- from . import layout, sync
37
+ from . import debrief, events, layout, sync
32
38
  from .git_runner import GitRunner, run_git
39
+ from .publication import WriterError
33
40
 
34
41
  SCHEMA_VERSION = 1
35
42
  RUNTIME_CLAUDE = "claude"
@@ -46,6 +53,8 @@ RESULT_OK = "ok"
46
53
  RESULT_DEGRADED = "degraded"
47
54
  PULL_NOT_REQUESTED = "not_requested"
48
55
  PULL_ERROR = "error"
56
+ #: The most of an event the author check reads; the frontmatter sits at its head.
57
+ _EVENT_HEAD_BYTES = 64 * 1024
49
58
 
50
59
 
51
60
  def build_manifest(
@@ -72,7 +81,7 @@ def build_manifest(
72
81
  warnings: List[str] = list(notes)
73
82
  files: List[Dict[str, Any]] = []
74
83
 
75
- sync_info, pull_info = _sync(home, pull, runner, warnings)
84
+ sync_info, pull_info = _sync(home, pull, runner, warnings, runtime)
76
85
  if project is not None:
77
86
  try:
78
87
  layout.validate_project_name(project)
@@ -154,14 +163,14 @@ def _entry(home: Path, path: Path, tier: str, name: str) -> Dict[str, Any]:
154
163
 
155
164
 
156
165
  def _sync(
157
- home: Path, pull: bool, runner: Optional[GitRunner], warnings: List[str]
166
+ home: Path, pull: bool, runner: Optional[GitRunner], warnings: List[str], runtime: str
158
167
  ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
159
168
  marker = sync.is_configured(home)
160
169
  info: Dict[str, Any] = {"marker": marker, "last_commit_at_utc": None}
161
170
  pull_info: Dict[str, Any] = {"requested": pull, "status": PULL_NOT_REQUESTED, "ok": True, "lines": []}
162
171
  if pull:
163
172
  try:
164
- outcome = sync.pull(home, runner=runner)
173
+ outcome = sync.pull(home, agent=runtime, runner=runner, hold=lambda path: _held(home, path, runtime))
165
174
  pull_info = {"requested": True, "status": outcome.status, "ok": outcome.ok, "lines": list(outcome.lines)}
166
175
  except (sync.SyncError, OSError) as exc:
167
176
  pull_info = {"requested": True, "status": PULL_ERROR, "ok": False, "lines": [f"memory pull: {exc}"]}
@@ -176,6 +185,40 @@ def _sync(
176
185
  return info, pull_info
177
186
 
178
187
 
188
+ def _held(home: Path, relative: str, runtime: str) -> bool:
189
+ """Whether ``relative`` names an author other than ``runtime``; no author named means not held."""
190
+ author = author_of(home, relative)
191
+ return author is not None and author != runtime
192
+
193
+
194
+ def author_of(home: Path, relative: str) -> Optional[str]:
195
+ """The agent a memory path names as its author, or ``None`` when it names none.
196
+
197
+ A debrief, ``org-memory/debriefs/<project>/<YYYY>/<MM>/<YYYYMMDD>-<agent>-<session>.md``,
198
+ names it in the filename: the session id has no dash, so the last dash
199
+ splits the agent from it whatever dashes the agent's own name carries. An
200
+ event, ``org-memory/events/<name>.md``, names it in the frontmatter
201
+ ``agent:`` field. Anything else, and anything that does not parse, names
202
+ no one.
203
+ """
204
+ parts = relative.split("/")
205
+ if parts[:2] == [layout.ORG.pattern, "debriefs"] and len(parts) == 6 and parts[5].endswith(".md"):
206
+ date, _, rest = parts[5][: -len(".md")].partition("-")
207
+ agent, _, session = rest.rpartition("-")
208
+ if len(date) == 8 and date.isdigit() and debrief.SESSION_RE.match(session) and debrief.AGENT_RE.match(agent):
209
+ return agent
210
+ return None
211
+ if parts[:2] == [layout.ORG.pattern, events.EVENTS_DIR] and len(parts) == 3 and parts[2].endswith(".md"):
212
+ try:
213
+ with open(home / relative, "rb") as handle:
214
+ frontmatter, _ = debrief.split_record(handle.read(_EVENT_HEAD_BYTES))
215
+ except (OSError, ValueError, WriterError):
216
+ return None
217
+ agent = frontmatter.get("agent", "")
218
+ return agent if debrief.AGENT_RE.match(agent) else None
219
+ return None
220
+
221
+
179
222
  # --- rendering --------------------------------------------------------------
180
223
 
181
224