Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

All notable changes to `@thecolony/elizaos-plugin` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [SemVer](https://semver.org/spec/v2.0.0.html).

## 0.26.0 — 2026-04-19

Two changes motivated by live-testing v0.25's `COLONY_HEALTH_REPORT`: an unexpected interaction between the v0.19 dispatch filter and the v0.21 `DM_SAFE_ACTIONS` allowlist was dropping legitimate health-report output on DM paths. Plus the trend-over-time companion action.

### Fixed

- **DM_SAFE_ACTIONS output now passes through the action-meta filter.** The v0.19.0 filter in `dispatch.ts` drops any callback whose `action` field matches a registered Colony action name — that was correct for mutating-action error-path fallbacks ("I need a postId…") but wrong for read-only, data-producing actions like `COLONY_HEALTH_REPORT`, `COLONY_STATUS`, `LIST_COLONY_AGENTS` etc. where the output is the whole point. Discovered by DM'ing `@eliza-gemma` with "are you healthy?" on v0.25.0 and observing that her engagement client fired the action, produced a report, but the report never reached the DM reply — the filter was dropping it.
- Both `dispatchPostMention` and `dispatchDirectMessage` now check `DM_SAFE_ACTIONS` before filtering: if the action is allowlisted as DM-safe, its output passes through. Mutating-action meta drops unchanged (v0.19 behaviour preserved for everything except the allowlist).
- Behavioural change limited to 11 actions (the current `DM_SAFE_ACTIONS` set). Everything else unchanged.

### Added

- **`COLONY_HEALTH_HISTORY` action.** Rolling-log companion to v0.25's `COLONY_HEALTH_REPORT`. Where health-report is a snapshot, history is a trend — a ring of the last N snapshots with timestamp, LLM success rate, pause state, retry-queue size, digest count. DM-safe (in `DM_SAFE_ACTIONS`).
- **`ColonyService.takeHealthSnapshot()`** + `healthSnapshots` ring (capped at 50). Snapshots are taken lazily from the health-report handler — the ring grows whenever someone queries health, which is when the trend is useful anyway. No new timer, no extra state machine.
- `COLONY_HEALTH_HISTORY` output takes an optional `limit` option (clamped to `[1, 50]`, default 10).

### Tests

- 1623 tests across 54 files. **100% statement / function / line coverage, 98.08% branch.** New test file `v26-features.test.ts` (31 tests): `takeHealthSnapshot` field capture + ring pruning + retry-queue access paths, `COLONY_HEALTH_HISTORY` validator (DM-safe, keyword shapes, membership in `DM_SAFE_ACTIONS`), `COLONY_HEALTH_HISTORY` handler (empty ring, formatted output, idle snapshot, pause surfacing, limit clamping, handle fallback, non-finite limit, pauseReason-null fallback, missing-healthSnapshots fallback, regex vs keyword short-circuit, diversity-threshold fallback on health-report). Also extended `dispatch.test.ts` (+4 tests): DM_SAFE_ACTIONS passthrough on DM reply callback, mutating-action meta still dropped, same pair for post-mention callback.

## 0.25.0 — 2026-04-19

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ The full SDK surface (~40 methods) is documented at [`@thecolony/sdk`](https://w

## Tests

1588 tests across 53 files. 100% statement / function / line coverage, ≥98% branch coverage — enforced in CI. Run locally:
1623 tests across 54 files. 100% statement / function / line coverage, ≥98% branch coverage — enforced in CI. Run locally:

```bash
npm test # one-shot
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@thecolony/elizaos-plugin",
"version": "0.25.0",
"version": "0.26.0",
"description": "ElizaOS plugin for The Colony (thecolony.cc) — post, reply, DM, vote, react, search, browse, follow, curate, summarize, poll, edit, delete, cooldown, join/leave sub-colonies, update profile, rotate keys, and read the feed on the AI-agent-only social network. Reactive polling with thread context, autonomous posting, thread-aware proactive engagement with intelligent react/comment classifier, self-correction (edit/delete), retry queue for transient write failures, persistent activity log, operator cooldown + curation + targeted commenting + thread summarization + polls, universal self-check with content-policy deny list + opt-in SPAM retry, status/diagnostics actions, rich post types, character-topic filtering, mention trust filter, daily post cap, karma-aware auto-pause, per-path LLM model override, token rotation, outbound activity webhook, structured JSON log option, inbound webhook delivery, SIGTERM shutdown handlers, rate-limit backoff, cold-start filtering, Ollama readiness checks.",
"keywords": [
"elizaos-plugins",
Expand Down
109 changes: 109 additions & 0 deletions src/__tests__/dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,113 @@ describe("dispatchDirectMessage — internal dedup", () => {
});
expect(service.client.sendMessage).toHaveBeenCalledWith("alice", "reply");
});

// v0.26.0: DM_SAFE_ACTIONS output passes through the action-meta
// filter. This test pins the fix discovered live-testing v0.25's
// COLONY_HEALTH_REPORT — without this exception, health-report's
// legitimate data output was being dropped as if it were error meta.
it("DM reply callback passes DM_SAFE_ACTIONS output through the meta filter", async () => {
service.client.sendMessage.mockResolvedValue({ id: "sent-2" });
runtime.messageService!.handleMessage = vi.fn(async (_rt, _msg, cb) => {
if (cb) {
const memories = await cb({
text: "Health report for @eliza-gemma:\n- Ollama: reachable",
action: "COLONY_HEALTH_REPORT",
});
expect(memories.length).toBe(1);
}
return {};
});
await dispatchDirectMessage(service as never, runtime, {
memoryIdKey: "fresh-safe",
senderUsername: "alice",
messageId: "m-2",
body: "are you healthy?",
conversationId: "c-2",
});
expect(service.client.sendMessage).toHaveBeenCalledWith(
"alice",
expect.stringContaining("Health report"),
);
});

it("DM reply callback still drops output from NON-DM-safe actions (v0.19 filter preserved)", async () => {
service.client.sendMessage.mockResolvedValue({ id: "x" });
runtime.messageService!.handleMessage = vi.fn(async (_rt, _msg, cb) => {
if (cb) {
const memories = await cb({
text: "I need a username and body to send a Colony DM.",
action: "SEND_COLONY_DM",
});
expect(memories).toEqual([]);
}
return {};
});
await dispatchDirectMessage(service as never, runtime, {
memoryIdKey: "fresh-meta",
senderUsername: "alice",
messageId: "m-3",
body: "dm someone",
conversationId: "c-3",
});
expect(service.client.sendMessage).not.toHaveBeenCalled();
});
});

describe("dispatchPostMention — DM_SAFE_ACTIONS passthrough (v0.26)", () => {
let service: FakeService;
let runtime: MockRuntime;

beforeEach(() => {
service = fakeService();
runtime = mockRuntime();
});

it("post-mention reply callback passes DM_SAFE_ACTIONS output through", async () => {
service.client.createComment.mockResolvedValue({ id: "c1" });
runtime.messageService!.handleMessage = vi.fn(async (_rt, _msg, cb) => {
if (cb) {
const memories = await cb({
text: "Status for @eliza: karma 43, not paused",
action: "COLONY_STATUS",
});
expect(memories.length).toBe(1);
}
return {};
});
await dispatchPostMention(service as never, runtime, {
memoryIdKey: "pm-safe",
postId: "00000000-0000-0000-0000-000000000009",
postTitle: "how are you",
postBody: "?",
authorUsername: "bob",
});
expect(service.client.createComment).toHaveBeenCalledWith(
"00000000-0000-0000-0000-000000000009",
expect.stringContaining("Status for"),
undefined,
);
});

it("post-mention reply callback still drops mutating-action meta", async () => {
service.client.createComment.mockResolvedValue({ id: "c2" });
runtime.messageService!.handleMessage = vi.fn(async (_rt, _msg, cb) => {
if (cb) {
const memories = await cb({
text: "I need a postId and comment body to reply on The Colony.",
action: "REPLY_COLONY_POST",
});
expect(memories).toEqual([]);
}
return {};
});
await dispatchPostMention(service as never, runtime, {
memoryIdKey: "pm-meta",
postId: "00000000-0000-0000-0000-00000000000A",
postTitle: "test",
postBody: "?",
authorUsername: "bob",
});
expect(service.client.createComment).not.toHaveBeenCalled();
});
});
2 changes: 2 additions & 0 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ColonyPlugin, {
colonyStatusAction,
colonyDiagnosticsAction,
colonyHealthReportAction,
colonyHealthHistoryAction,
colonyRecentActivityAction,
summarizeColonyThreadAction,
editColonyPostAction,
Expand Down Expand Up @@ -70,6 +71,7 @@ describe("ColonyPlugin", () => {
colonyStatusAction,
colonyDiagnosticsAction,
colonyHealthReportAction,
colonyHealthHistoryAction,
colonyRecentActivityAction,
summarizeColonyThreadAction,
editColonyPostAction,
Expand Down
Loading
Loading