diff --git a/README.md b/README.md index 0882680..7010830 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,8 @@ Activation is environment-bound rather than a tracked `enabled: true`. The consu pnpm thought watch --producer-only --root --source filesystem:coil ``` +`deploy/systemd/thoughtstream-coil-filesystem.service` is the persistent producer template. It grants read-only home access, write access only to the private ThoughtStream root, loads no credential file, and excludes Obsidian's private `.obsidian/` application state. + The declaration serializes one agent operation at a time and uses durable 30-second, hourly, and daily accounting windows. Enabling it against an existing source intentionally replays that declaration version from the beginning; run a bounded canary before full activation. Observed Agent SDK 0.5.7 behavior: exact `summarySearch` plus conversation management is sufficient to recover a remote conversation created before Jazz binding evidence settles, and local API-backed sessions successfully enforce the memory-root/no-tools profile. The local management transport keeps its SDK-owned App Server pooled and exposes no top-level client shutdown method, so one-shot probes do not naturally terminate after their result. Long-running production consumers therefore own the App Server in one systemd control group; bounded canaries require an external process-group supervisor. This is the same lifecycle gap tracked in [letta-agent-sdk#245](https://github.com/letta-ai/letta-agent-sdk/issues/245). diff --git a/deploy/systemd/thoughtstream-coil-filesystem.service b/deploy/systemd/thoughtstream-coil-filesystem.service new file mode 100644 index 0000000..552df33 --- /dev/null +++ b/deploy/systemd/thoughtstream-coil-filesystem.service @@ -0,0 +1,23 @@ +[Unit] +Description=ThoughtStream read-only Coil filesystem producer +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=%h/code/thought-stream +Environment=THOUGHTSTREAM_ROOT=%h/.local/share/thoughtstream/live +EnvironmentFile= +ExecStart=/usr/bin/env pnpm thought watch --producer-only --root "%h/Documents/The Coil" --source filesystem:coil +Restart=always +RestartSec=5 +TimeoutStopSec=30 +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=read-only +ReadWritePaths=%h/.local/share/thoughtstream/live +UMask=0077 + +[Install] +WantedBy=default.target diff --git a/src/connectors/filesystem.ts b/src/connectors/filesystem.ts index 1afef6d..ea38dce 100644 --- a/src/connectors/filesystem.ts +++ b/src/connectors/filesystem.ts @@ -45,6 +45,7 @@ interface FileObservation { const defaultIgnores = [ "**/.git/**", + "**/.obsidian/**", "**/.thoughtstream/**", "**/node_modules/**", "**/.DS_Store", diff --git a/test/filesystem.test.ts b/test/filesystem.test.ts index d7f454e..18664c3 100644 --- a/test/filesystem.test.ts +++ b/test/filesystem.test.ts @@ -19,6 +19,8 @@ describe("FilesystemConnector", () => { roots.push(project); const vault = path.join(project, "vault"); await fs.mkdir(vault); + await fs.mkdir(path.join(vault, ".obsidian")); + await fs.writeFile(path.join(vault, ".obsidian", "workspace.json"), "{\"private\":true}\n"); await fs.writeFile(path.join(vault, "alpha.md"), "---\nid: alpha\n---\n# Alpha\n\n[[Beta]]\n"); await fs.writeFile(path.join(vault, "beta.md"), "# Beta\n"); @@ -28,6 +30,8 @@ describe("FilesystemConnector", () => { const initial = await connector.scan(store); expect(initial.added).toBe(2); + expect((await store.listCurrentDocuments("filesystem:fixture")).map((document) => document.path)) + .not.toContain(".obsidian/workspace.json"); expect(initial.events.map((event) => event.type)).toEqual([ "stream.thought.source.file.added", "stream.thought.source.file.added",