diff --git a/README.md b/README.md index 89ee590..43cf8a2 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ Tracked account lists live in `config/x-cameron-public.yaml` and `config/x-publi Public management uses a separate app-bearer environment. Sensitive outbound likes require the separate `x-user-management` compartment and user-context OAuth custody; the tracked source and batch stay disabled until that grant and its refresh procedure exist. Status and plan are read-only. Register, apply, replay, and delete require hashes or ids copied from current readback, and none runs during receiver startup. -The existing resident Stream agent receives one mixed-privacy activity window at most every ten minutes. The deterministic batch includes complete per-source prefixes from the configured filesystem, ATProto, Telegram, email, agent-message, and X inputs, then promotes the batch to its most-private member. The resident shares one persistent conversation with direct Telegram turns, but direct replies remain prose while activity windows require strict JSON. A high-importance notification tuple is only a proposal; the separately configured Telegram dispatcher must still match the exact agent/source route, claim the run, deliver the message, and append the receipt. See [`spec/agents.md`](spec/agents.md). +The existing resident Stream agent receives one mixed-privacy activity window at most every ten minutes. The deterministic batch includes complete per-source prefixes from the configured filesystem, ATProto, non-conversational Telegram activity, email, agent-message, and X inputs, then promotes the batch to its most-private member. Direct Stream Telegram messages are excluded because the same persistent conversation already receives them as direct turns; duplicating a partial copy inside the activity window produces no new evidence. Direct replies remain prose while activity windows require strict JSON. A high-importance notification tuple is only a proposal; the separately configured Telegram dispatcher must still match the exact agent/source route, claim the run, deliver the message, and append the receipt. See [`spec/agents.md`](spec/agents.md). ## Consumer declarations diff --git a/spec/agents.md b/spec/agents.md index cc38497..c79670c 100644 --- a/spec/agents.md +++ b/spec/agents.md @@ -42,7 +42,7 @@ The resident keeps measured token accounting and a conservative full-conversatio Exactly one execution path owns direct Telegram replies. The active path is `resident-letta-conversation@6`: one persistent Letta Cloud agent main conversation receives the current webhook event as a bounded single trigger and owns its prior interaction internally. A direct turn includes projected text plus at most one current Telegram image artifact; the trusted parent revalidates the content-addressed file before passing it to the SDK as native multimodal content. Missing, malformed, changed, escaped, or otherwise invalid image evidence fails closed before the turn is sent. The persistent private SDK conversation may retain the validated multimodal message as remote history; local Jazz events, traces, incidents, and projections retain only the opaque artifact evidence. The Pi `telegram-conversation@22` and reciprocal `telegram-conversation-compactor@4` are disabled together, so one source event cannot produce competing Letta and Tinker replies. Switching paths is a coordinated declaration, credential, consumer, and dispatcher deployment; changing a model string alone is not a cutover. -The same `resident-letta-conversation@6` also consumes `batch:stream-activity`. The deterministic ten-minute batch merges complete prefixes from several source namespaces into one ordered window and sets the batch privacy to its most-private member. Direct Telegram turns keep conversation-text output. Activity-window turns use strict JSON under the observation contract. Both routes serialize through the resident's one persistent main conversation. +The same `resident-letta-conversation@6` also consumes `batch:stream-activity`. The deterministic ten-minute batch merges complete prefixes from several source namespaces into one ordered window and sets the batch privacy to its most-private member. Direct Stream Telegram messages are deliberately excluded from that batch because the resident has already received them in the same persistent conversation; non-conversation, reaction, and correction events may still join the window. Direct Telegram turns keep conversation-text output. Activity-window turns use strict JSON under the observation contract. Both routes serialize through the resident's one persistent main conversation. An activity-window output may propose scarce human attention through the ordinary observation contract. Telegram eligibility requires the configured resident/source tuple, `importance: high`, the exact `notify-cameron` tag, and a recommendation targeting `cameron-telegram` with proposed action `notify`. The proposal stays inert until the dispatcher claims it. Direct Telegram replies use a disjoint source route and do not need the notification tuple. Ordinary window observations remain in Jazz. diff --git a/test/x-manifest.test.ts b/test/x-manifest.test.ts index e623a54..2db5d17 100644 --- a/test/x-manifest.test.ts +++ b/test/x-manifest.test.ts @@ -39,6 +39,7 @@ describe("X webhook manifest", () => { }); expect(loaded.manifest.batches.find((batch) => batch.id === "stream-activity-ten-minute")).toMatchObject({ enabled: false, + version: 2, input: { eventTypes: expect.arrayContaining(["stream.thought.source.atproto.commit", "stream.thought.source.x.activity"]), sourceIds: expect.arrayContaining(["jetstream:cameron-atproto", "x:cameron-public", "x:cameron-private", "x:public-watch"]), @@ -48,6 +49,13 @@ describe("X webhook manifest", () => { maxAgeMs: 600_000, privacy: "most-private", }); + const streamActivity = loaded.manifest.batches.find((batch) => batch.id === "stream-activity-ten-minute"); + expect(streamActivity?.input.eventTypes).not.toContain("stream.thought.source.telegram.message"); + expect(streamActivity?.input.eventTypes).toEqual(expect.arrayContaining([ + "stream.thought.source.telegram.nonconversation", + "stream.thought.source.telegram.reaction", + "stream.thought.source.telegram.correction", + ])); const telegram = loaded.manifest.sources.find((source) => source.kind === "telegram-webhook"); expect(telegram?.kind === "telegram-webhook" ? telegram.channels[0]?.notifications : undefined).toMatchObject({ allowedSources: ["telegram:thoughtstream-bot-webhook", "batch:stream-activity"], diff --git a/thoughtstream.yaml b/thoughtstream.yaml index 1250b8a..ec4feaa 100644 --- a/thoughtstream.yaml +++ b/thoughtstream.yaml @@ -45,7 +45,7 @@ batches: replay: now pollIntervalMs: 1000 - id: stream-activity-ten-minute - version: 1 + version: 2 enabled: false input: eventTypes: @@ -56,7 +56,6 @@ batches: - stream.thought.source.rss.item - stream.thought.source.atproto.commit - stream.thought.source.email.observed - - stream.thought.source.telegram.message - stream.thought.source.telegram.nonconversation - stream.thought.source.telegram.reaction - stream.thought.source.telegram.correction