diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
deleted file mode 100644
index 88567914..00000000
--- a/.github/workflows/claude.yml
+++ /dev/null
@@ -1,95 +0,0 @@
-name: Claude Code
-
-on:
- issue_comment:
- types: [created]
- pull_request_review_comment:
- types: [created]
- pull_request:
- types: [opened, synchronize, ready_for_review, reopened]
- pull_request_review:
- types: [submitted]
-
-jobs:
- claude-review:
- if: github.event_name == 'pull_request'
- runs-on: ubuntu-latest
- timeout-minutes: 15
- concurrency:
- group: claude-review-${{ github.event.pull_request.number }}
- cancel-in-progress: true
- permissions:
- contents: read
- pull-requests: read
- issues: read
- id-token: write
- actions: read
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 1
-
- - name: Run Claude Code Review
- id: claude-review
- uses: anthropics/claude-code-action@v1
- with:
- claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
- plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
- plugins: 'code-review@claude-code-plugins'
- prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
- claude_args: '--model claude-opus-4-6'
- # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
- # or https://code.claude.com/docs/en/cli-reference for available options
-
- claude:
- if: |
- (github.event_name == 'issue_comment' &&
- github.event.issue.pull_request &&
- contains(github.event.comment.body, '@claude') &&
- (github.event.comment.author_association == 'OWNER' ||
- github.event.comment.author_association == 'MEMBER' ||
- github.event.comment.author_association == 'COLLABORATOR')) ||
- (github.event_name == 'pull_request_review_comment' &&
- contains(github.event.comment.body, '@claude') &&
- (github.event.comment.author_association == 'OWNER' ||
- github.event.comment.author_association == 'MEMBER' ||
- github.event.comment.author_association == 'COLLABORATOR')) ||
- (github.event_name == 'pull_request_review' &&
- contains(github.event.review.body, '@claude') &&
- (github.event.review.author_association == 'OWNER' ||
- github.event.review.author_association == 'MEMBER' ||
- github.event.review.author_association == 'COLLABORATOR'))
- runs-on: ubuntu-latest
- timeout-minutes: 15
- permissions:
- contents: read
- pull-requests: read
- issues: read
- id-token: write
- actions: read # Required for Claude to read CI results on PRs
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 1
-
- - name: Run Claude Code
- id: claude
- uses: anthropics/claude-code-action@v1
- with:
- claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
- claude_args: '--model claude-opus-4-6'
-
- # This is an optional setting that allows Claude to read CI results on PRs
- additional_permissions: |
- actions: read
-
- # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
- # prompt: 'Update the pull request description to include a summary of changes.'
-
- # Optional: Add claude_args to customize behavior and configuration
- # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
- # or https://code.claude.com/docs/en/cli-reference for available options
- # claude_args: '--allowed-tools Bash(gh pr:*)'
diff --git a/docs/docs.json b/docs/docs.json
index 35edfd38..e3ca5184 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -64,6 +64,7 @@
"group": "Advanced",
"pages": [
"v3/documentation/features/advanced/overview",
+ "v3/documentation/features/advanced/dreaming",
"v3/documentation/features/advanced/queue-status",
"v3/documentation/features/advanced/reasoning-configuration",
"v3/documentation/features/advanced/representation-scopes",
diff --git a/docs/v3/documentation/core-concepts/reasoning.mdx b/docs/v3/documentation/core-concepts/reasoning.mdx
index 2ea6b1d8..750ccc77 100644
--- a/docs/v3/documentation/core-concepts/reasoning.mdx
+++ b/docs/v3/documentation/core-concepts/reasoning.mdx
@@ -64,6 +64,16 @@ The reasoning outputs--conclusions, summaries, peer cards--are stored as part of
The diagram above shows how agents write messages to Honcho, which triggers reasoning that updates peer representations. Agents can then query representations to get additional context for their next response.
+### Token Batching
+
+Rather than running inference on every individual message, Honcho accumulates messages in the queue and processes them as a batch once the total token count of pending messages for a given peer representation crosses a threshold--roughly **1,000 tokens** at the current batch size. This keeps ingestion costs down, since Honcho charges based on reasoning passes, and ensures each pass has a meaningful amount of context to work with. At ~1,000 tokens the batch comfortably fits in the context window of any modern LLM, so no content is lost.
+
+If a user sends several short messages in a row (e.g., "yes", "ok", "sounds good"), those messages sit in the queue until enough content has accumulated. Once the threshold is met, the full batch is processed together in a single reasoning call.
+
+
+This batching only applies to **representation** tasks (conclusion extraction). Summary and dream tasks have their own scheduling logic and are not subject to the token threshold.
+
+
## Balances & Design Choices
Off-the-shelf LLMs can perform formal logical reasoning, but they aren't optimized for it. Honcho uses custom models trained specifically for logical rigor (following formal reasoning rules rather than plausible-sounding text), structured output (consistent JSON schema with premises and conclusions), and efficiency (smaller, faster models tuned for this specific task). This allows Honcho to reason more reliably and at lower cost than general-purpose frontier LLMs.
diff --git a/docs/v3/documentation/features/advanced/dreaming.mdx b/docs/v3/documentation/features/advanced/dreaming.mdx
new file mode 100644
index 00000000..7110ec36
--- /dev/null
+++ b/docs/v3/documentation/features/advanced/dreaming.mdx
@@ -0,0 +1,140 @@
+---
+title: 'Dreaming'
+description: 'How Honcho continuously improves memory through autonomous consolidation'
+icon: 'cloud-moon'
+---
+
+
+Dreaming is an experimental feature under active development. The scheduling heuristics, specialist behavior, and configuration options described here are subject to change as we iterate on the approach.
+
+
+Honcho's reasoning system extracts conclusions from every message as it arrives. Over time, this produces a large body of knowledge--some of which is redundant, outdated, or missing higher-order patterns that only become visible across many interactions. **Dreaming** is the process that addresses this: an autonomous, periodic consolidation cycle that refines the peer representation by reasoning over existing conclusions.
+
+Think of it like sleep for a memory system. The "waking" reasoning process captures what happened. The dreaming process reflects on what it all means.
+
+## What Dreaming Does
+
+A dream cycle runs two specialized agents in sequence:
+
+### 1. Deduction
+
+The deduction specialist performs logical inference over existing conclusions. It autonomously explores the observation space and looks for:
+
+- **Knowledge updates**: When the same fact has changed over time (e.g., "works at Company A" followed later by "works at Company B"), it deletes the outdated conclusion and creates a new one reflecting the current state.
+- **Logical implications**: Conclusions that follow necessarily from existing premises but weren't captured during real-time processing.
+- **Contradictions**: Conflicting conclusions that need resolution.
+- **Peer card updates**: Key biographical facts (name, location, occupation) that should be recorded on the peer card for quick access.
+
+### 2. Induction
+
+The induction specialist identifies patterns across multiple conclusions. It looks for:
+
+- **Behavioral tendencies**: Recurring behaviors observed across different contexts (e.g., "tends to reschedule meetings when stressed").
+- **Preferences**: Consistent choices that indicate underlying preferences.
+- **Personality traits**: Stable characteristics inferred from multiple data points.
+- **Correlations**: Relationships between different aspects of behavior.
+
+Inductive conclusions require evidence from at least two source conclusions--patterns need more than a single data point. Each pattern is assigned a confidence level based on the number of supporting observations.
+
+## When Dreams Are Scheduled
+
+Dreams are triggered automatically based on a set of heuristics designed to balance freshness with efficiency:
+
+### Conditions
+
+All of the following must be true for a dream to be scheduled:
+
+1. **Document threshold**: At least 50 new conclusions have been created since the last dream for that peer representation.
+2. **Minimum cooldown**: At least 8 hours have passed since the last dream for that peer representation.
+3. **Dreaming is enabled**: The workspace and/or session configuration has `dream.enabled` set to `true` (the default).
+
+### Idle timeout
+
+When the threshold conditions are met, a dream is **not** immediately executed. Instead, a timer is set (default: 60 minutes) that waits for user inactivity. If new messages arrive during the waiting period, the pending dream is cancelled and the timer resets. This prevents dreaming while the user is actively interacting, ensuring the system consolidates only after the conversation has settled.
+
+Once the idle timeout expires without interruption, the dream task is enqueued for processing.
+
+### Manual scheduling
+
+You can also trigger a dream explicitly via the API:
+
+
+```python Python
+honcho.workspaces.schedule_dream(
+ observer="user-peer-name",
+ observed="user-peer-name",
+)
+```
+```typescript TypeScript
+await honcho.workspaces.scheduleDream({
+ observer: "user-peer-name",
+ observed: "user-peer-name",
+});
+```
+
+
+Manual dreams bypass the threshold and cooldown checks, but are still subject to deduplication--if a dream is already pending or in progress for the same peer representation, the request is a no-op.
+
+## Scope
+
+Dreams operate at the **peer representation** level--specifically, a (workspace, observer, observed) tuple. This means:
+
+- A dream consolidates conclusions for a specific observer's view of a specific observed peer.
+- In the common case of self-observation (where the observer and observed are the same peer), the dream consolidates that peer's own representation.
+- Dreams do not span across workspaces or across different peer pairs.
+
+## Deduplication and Safety
+
+The system includes several safeguards to prevent wasted work:
+
+- **No concurrent dreams**: If a dream is already being processed for a given peer representation, a new one will not be enqueued.
+- **No duplicate pending dreams**: If a dream is already queued and waiting, a second enqueue request is skipped.
+- **Cancellation on new activity**: When new messages arrive for a peer, any pending (not yet started) dream for that peer is cancelled. This ensures the dream always runs on the most up-to-date set of conclusions.
+
+## Configuration
+
+Dreams can be enabled or disabled at the workspace or session level:
+
+
+```python Python
+# Disable dreams for a workspace
+honcho.set_configuration({
+ "dream": {"enabled": False}
+})
+
+# Disable dreams for a specific session
+session = honcho.session("my-session", config={
+ "dream": {"enabled": False}
+})
+```
+```typescript TypeScript
+// Disable dreams for a workspace
+await honcho.setConfiguration({
+ dream: { enabled: false }
+});
+
+// Disable dreams for a specific session
+const session = await honcho.session("my-session", {
+ config: {
+ dream: { enabled: false }
+ }
+});
+```
+
+
+Dreaming is automatically disabled if reasoning itself is disabled, since there would be no conclusions to consolidate.
+
+
+
+ Learn how Honcho reasons over messages to produce conclusions
+
+
+ Full configuration reference for reasoning, summaries, and dreams
+
+
+ Monitor dream tasks alongside other background processing
+
+
+ API reference for manually triggering dreams
+
+
diff --git a/docs/v3/documentation/features/advanced/overview.mdx b/docs/v3/documentation/features/advanced/overview.mdx
index 77f17a7b..2a58a854 100644
--- a/docs/v3/documentation/features/advanced/overview.mdx
+++ b/docs/v3/documentation/features/advanced/overview.mdx
@@ -9,6 +9,7 @@ Advanced features give you fine-grained control over Honcho's behavior and imple
## Configuration & Monitoring
+- [Dreaming](/v3/documentation/features/advanced/dreaming) - Autonomous memory consolidation and self-improvement
- [Queue Status](/v3/documentation/features/advanced/queue-status) - Monitor background processing and reasoning tasks
- [Configuration](/v3/documentation/features/advanced/reasoning-configuration) - Configure reasoning models and behavior
- [Summarizer](/v3/documentation/features/advanced/summarizer) - Automatic session summarization
diff --git a/docs/v3/documentation/features/advanced/queue-status.mdx b/docs/v3/documentation/features/advanced/queue-status.mdx
index 6c4598cd..7c4d363c 100644
--- a/docs/v3/documentation/features/advanced/queue-status.mdx
+++ b/docs/v3/documentation/features/advanced/queue-status.mdx
@@ -74,6 +74,26 @@ work_units will be processed in parallel
- If local representations are turned in a Session then a message will
generate an additional work unit for every peer that has `observe_others=True`
+### Tracked task types
+
+The queue status endpoint reports on the following task types:
+
+| Task Type | Description |
+|---|---|
+| **representation** | Memory formation — the deriver processes messages and extracts observations about peers |
+| **summary** | Session summarization — creates short and long summaries at configurable message intervals |
+| **dream** | Memory consolidation — explores and consolidates observations to improve memory quality |
+
+Internal infrastructure tasks (such as webhook delivery, resource deletion, and
+vector reconciliation) are **not** included in queue status counts.
+
+
+**Completed counts are not lifetime totals.** Honcho periodically cleans up
+processed queue items to keep the queue table lean. As a result,
+`completed_work_units` reflects items completed since the last cleanup cycle,
+not the total number of items ever processed.
+
+
The `queue_status` method can take additional
parameters to scope the status to a specific work unit:
diff --git a/src/crud/deriver.py b/src/crud/deriver.py
index 66c672da..0852a479 100644
--- a/src/crud/deriver.py
+++ b/src/crud/deriver.py
@@ -22,6 +22,12 @@ async def get_queue_status(
"""
Get the processing queue status, optionally filtered by observer, sender, and/or session.
+ Only tracks user-facing task types: representation, summary, and dream.
+ Internal infrastructure tasks (reconciler, webhook, deletion) are excluded.
+
+ Note: completed_work_units reflects items since the last periodic queue
+ cleanup, not lifetime totals.
+
Args:
db: Database session
workspace_name: Name of the workspace
@@ -69,6 +75,10 @@ async def get_deriver_status(
)
+# Task types surfaced by the queue status endpoint.
+_TRACKED_TASK_TYPES = ("representation", "summary", "dream")
+
+
def _build_queue_status_query(
workspace_name: str,
session_name: str | None,
@@ -119,6 +129,9 @@ def _build_queue_status_query(
stmt = stmt.where(models.QueueItem.workspace_name == workspace_name)
+ # Only include user-facing task types
+ stmt = stmt.where(models.QueueItem.task_type.in_(_TRACKED_TASK_TYPES))
+
if session_name is not None:
stmt = stmt.join(
models.Session, models.QueueItem.session_id == models.Session.id
diff --git a/src/routers/workspaces.py b/src/routers/workspaces.py
index d4715c33..7f722c5c 100644
--- a/src/routers/workspaces.py
+++ b/src/routers/workspaces.py
@@ -171,6 +171,11 @@ async def get_queue_status(
"""
Get the processing queue status for a Workspace, optionally scoped to an observer, sender,
and/or session.
+
+ Only tracks user-facing task types (representation, summary, dream).
+ Internal infrastructure tasks (reconciler, webhook, deletion) are excluded.
+ Note: completed counts reflect items since the last periodic queue cleanup,
+ not lifetime totals.
"""
try:
return await crud.get_queue_status(
diff --git a/src/schemas.py b/src/schemas.py
index 07c24576..55685e64 100644
--- a/src/schemas.py
+++ b/src/schemas.py
@@ -799,10 +799,19 @@ class SessionQueueStatus(BaseModel):
class QueueStatus(BaseModel):
- """Aggregated processing queue status."""
+ """Aggregated processing queue status.
+
+ Tracks user-facing task types only: representation, summary, and dream.
+ Internal infrastructure tasks (reconciler, webhook, deletion) are excluded.
+
+ Note: completed_work_units reflects items since the last periodic queue
+ cleanup, not lifetime totals.
+ """
total_work_units: int = Field(description="Total work units")
- completed_work_units: int = Field(description="Completed work units")
+ completed_work_units: int = Field(
+ description="Completed work units (since last periodic cleanup)"
+ )
in_progress_work_units: int = Field(
description="Work units currently being processed"
)
diff --git a/tests/routes/test_queue_status.py b/tests/routes/test_queue_status.py
index 782aa3c1..af25077b 100644
--- a/tests/routes/test_queue_status.py
+++ b/tests/routes/test_queue_status.py
@@ -256,6 +256,64 @@ class TestDeriverStatusEndpoint:
)
assert session_totals == [1, 2, 3]
+ async def test_get_queue_status_excludes_internal_task_types(
+ self,
+ client: TestClient,
+ db_session: AsyncSession,
+ sample_data: tuple[models.Workspace, models.Peer],
+ ):
+ """Test that internal task types (reconciler, webhook, deletion) are excluded from counts"""
+ workspace, peer = sample_data
+ session = models.Session(workspace_name=workspace.name, name="test_session")
+ db_session.add(session)
+ await db_session.commit()
+ await db_session.refresh(session)
+
+ # Add one representation item (should be counted)
+ rep_payload = {
+ "observed": peer.name,
+ "observer": peer.name,
+ "task_type": "representation",
+ "workspace_name": workspace.name,
+ "session_name": session.name,
+ }
+ db_session.add(
+ models.QueueItem(
+ session_id=session.id,
+ task_type="representation",
+ work_unit_key=construct_work_unit_key(workspace.name, rep_payload),
+ payload=rep_payload,
+ processed=False,
+ workspace_name=workspace.name,
+ )
+ )
+
+ # Add internal task types (should NOT be counted)
+ for task_type in ("reconciler", "webhook", "deletion"):
+ internal_payload = {
+ "task_type": task_type,
+ "workspace_name": workspace.name,
+ }
+ db_session.add(
+ models.QueueItem(
+ session_id=None,
+ task_type=task_type,
+ work_unit_key=f"{task_type}:{workspace.name}:internal",
+ payload=internal_payload,
+ processed=False,
+ workspace_name=workspace.name,
+ )
+ )
+
+ await db_session.commit()
+
+ response = client.get(f"/v3/workspaces/{workspace.name}/queue/status")
+ assert response.status_code == 200
+ json_response = response.json()
+ # Only the representation item should appear
+ assert json_response["total_work_units"] == 1
+ assert json_response["pending_work_units"] == 1
+
async def test_get_deriver_status_empty_parameters(
self, client: TestClient, sample_data: tuple[models.Workspace, models.Peer]
):