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
2 changes: 1 addition & 1 deletion .ai/BOOTSTRAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Full validation before release: `npm run release:preflight`.
- Shared packages: @claw/shared-auth, @claw/shared-constants, @claw/shared-entitlements, @claw/shared-rabbitmq, @claw/shared-types, @claw/shared-utilities
- Events: 178 on `claw.events`
- Permissions: 51 · Env vars: 351
- API endpoints: 643 · Frontend pages: 143
- API endpoints: 644 · Frontend pages: 143

This file is generated. To change it, edit the renderer + policy sources and run
`npm run knowledge:build`.
7 changes: 6 additions & 1 deletion .ai/manifests/api-endpoints.json
Original file line number Diff line number Diff line change
Expand Up @@ -2331,6 +2331,11 @@
"route": "/health",
"source": "apps/claw-routing-service/src/modules/health/controllers/health.controller.ts"
},
{
"method": "GET",
"route": "/internal/router-models/context-window/:provider/:model",
"source": "apps/claw-routing-service/src/modules/router-models/controllers/model-context-window-internal.controller.ts"
},
{
"method": "GET",
"route": "/internal/router-models/costs/:provider/:model",
Expand Down Expand Up @@ -3253,5 +3258,5 @@
]
},
"generated": true,
"total": 643
"total": 644
}
6 changes: 5 additions & 1 deletion .ai/manifests/governance.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@
],
"skills": [
{
"bytes": 19868,
"bytes": 20103,
"file": "skills/00-index.md"
},
{
Expand Down Expand Up @@ -339,6 +339,10 @@
"bytes": 4776,
"file": "skills/add-workspace-connector.md"
},
{
"bytes": 8787,
"file": "skills/audit-conversational-context.md"
},
{
"bytes": 5494,
"file": "skills/backend-architecture-review.md"
Expand Down
18 changes: 9 additions & 9 deletions .ai/manifests/hashes.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
{
"generated": true,
"hashes": {
".ai/BOOTSTRAP.md": "67f2767a",
".ai/manifests/api-endpoints.json": "c1aec4ca",
".ai/BOOTSTRAP.md": "c2216623",
".ai/manifests/api-endpoints.json": "995a14e1",
".ai/manifests/data-ownership.json": "501d74dc",
".ai/manifests/docker-services.json": "255da189",
".ai/manifests/environment-variables.json": "33602cbe",
".ai/manifests/event-graph.json": "c1cd05bc",
".ai/manifests/frontend-routes.json": "a52b7ece",
".ai/manifests/governance.json": "ef960bf2",
".ai/manifests/i18n.json": "5321b991",
".ai/manifests/governance.json": "f71a269b",
".ai/manifests/i18n.json": "3befb756",
".ai/manifests/nginx-routes.json": "53c65be1",
".ai/manifests/packages.json": "cc928a4a",
".ai/manifests/permissions.json": "c87c2c04",
".ai/manifests/ports.json": "6b737943",
".ai/manifests/prisma-models.json": "ac082a39",
".ai/manifests/rabbitmq-events.json": "bcaeb411",
".ai/manifests/repository.json": "047d93d0",
".ai/manifests/services.json": "ee33ea47",
".ai/manifests/tests.json": "17a5bc52",
".ai/manifests/services.json": "9c92d2c4",
".ai/manifests/tests.json": "4b6be224",
".ai/manifests/workspace-dependency-graph.json": "80e5438b",
".ai/manifests/workspaces.json": "b5133e99",
".ai/packs/README.md": "2e64753b",
Expand All @@ -36,7 +36,7 @@
"apps/claw-agent-service/AGENTS.md": "8d90d82d",
"apps/claw-audit-service/AGENTS.md": "2342ed5d",
"apps/claw-auth-service/AGENTS.md": "6ff5cb9c",
"apps/claw-chat-service/AGENTS.md": "4a134984",
"apps/claw-chat-service/AGENTS.md": "ed858ddb",
"apps/claw-client-logs-service/AGENTS.md": "7d329099",
"apps/claw-connector-service/AGENTS.md": "45f37482",
"apps/claw-file-generation-service/AGENTS.md": "f247a3e4",
Expand All @@ -45,11 +45,11 @@
"apps/claw-health-service/AGENTS.md": "5ebb83b5",
"apps/claw-image-service/AGENTS.md": "ba82000d",
"apps/claw-llamacpp-service/AGENTS.md": "5514cb48",
"apps/claw-memory-service/AGENTS.md": "5357edef",
"apps/claw-memory-service/AGENTS.md": "9ddbf165",
"apps/claw-ollama-service/AGENTS.md": "13684e8e",
"apps/claw-payment-service/AGENTS.md": "e1e9991f",
"apps/claw-research-service/AGENTS.md": "c6c46e1f",
"apps/claw-routing-service/AGENTS.md": "dced4a0e",
"apps/claw-routing-service/AGENTS.md": "302993c4",
"apps/claw-server-logs-service/AGENTS.md": "fc1c971a",
"apps/claw-workspace-service/AGENTS.md": "bf6bb5c0",
"packages/shared-auth/AGENTS.md": "049afff2",
Expand Down
2 changes: 1 addition & 1 deletion .ai/manifests/i18n.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"approxKeyCount": 5001,
"approxKeyCount": 5015,
"generated": true,
"locales": [
"ar",
Expand Down
6 changes: 3 additions & 3 deletions .ai/manifests/services.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
"FileDeliveryRecord",
"MessageAttachment"
],
"testFiles": 117,
"testFiles": 125,
"testRunner": "jest"
},
{
Expand Down Expand Up @@ -320,7 +320,7 @@
"MemoryUsage",
"WorkspaceObjectEmbedding"
],
"testFiles": 12,
"testFiles": 14,
"testRunner": "jest"
},
{
Expand Down Expand Up @@ -421,7 +421,7 @@
{
"database": "postgresql",
"dir": "apps/claw-routing-service",
"endpointCount": 69,
"endpointCount": 70,
"internalDeps": [
"@claw/shared-constants",
"@claw/shared-entitlements",
Expand Down
6 changes: 3 additions & 3 deletions .ai/manifests/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
},
"claw-chat-service": {
"runner": "jest",
"testFiles": 117
"testFiles": 125
},
"claw-client-logs-service": {
"runner": "jest",
Expand Down Expand Up @@ -74,7 +74,7 @@
},
"claw-memory-service": {
"runner": "jest",
"testFiles": 12
"testFiles": 14
},
"claw-ollama-service": {
"runner": "jest",
Expand Down Expand Up @@ -102,5 +102,5 @@
}
},
"generated": true,
"total": 1109
"total": 1119
}
2 changes: 1 addition & 1 deletion apps/claw-chat-service/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ npm run dev
- Database: postgresql
- Prisma models: ChatMessage, ChatMessageContextReceipt, ChatShare, ChatShareMessage, ChatShareMessageAsset, ChatThread, FileDeliveryRecord, MessageAttachment
- API endpoints: 47 (see `.ai/manifests/api-endpoints.json`)
- Test files: 117 (jest)
- Test files: 125 (jest)
- Depends on: @claw/shared-constants, @claw/shared-entitlements, @claw/shared-rabbitmq, @claw/shared-types, @claw/shared-utilities

## Before editing
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- ADR-087 — cross-thread retrieval.
--
-- Default FALSE, and that is the whole privacy posture in one word: reaching
-- into a user's other conversations is opt-in. Every existing thread therefore
-- keeps behaving exactly as it does today after this migration runs.
ALTER TABLE "chat_threads"
ADD COLUMN "use_cross_thread_context" BOOLEAN NOT NULL DEFAULT false;

-- Stage 2 of retrieval reads recent messages for a set of candidate threads.
-- The existing single-column thread_id index cannot serve the ORDER BY, so
-- without this the read degrades to a scan as a user's history grows.
CREATE INDEX IF NOT EXISTS "chat_messages_thread_id_created_at_idx"
ON "chat_messages" ("thread_id", "created_at");
9 changes: 9 additions & 0 deletions apps/claw-chat-service/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ model ChatThread {
// === Integration V2 — per-thread memory/context switches ===
useMemory Boolean @default(true) @map("use_memory")
useContext Boolean @default(true) @map("use_context")
// === ADR-087 — cross-thread retrieval ===
// OFF by default, and deliberately so. Silently reaching into a user's other
// conversations is a privacy decision, not a quality tweak: it must be asked
// for. When off, nothing from another thread can enter the prompt except a
// durable memory the user's own memory preferences already allow.
useCrossThreadContext Boolean @default(false) @map("use_cross_thread_context")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

Expand Down Expand Up @@ -106,6 +112,9 @@ model ChatMessage {

@@index([threadId])
@@index([createdAt])
// Cross-thread retrieval reads a user's recent messages across many threads.
// Without this the stage-2 read is a scan of every message in the table.
@@index([threadId, createdAt])
@@map("chat_messages")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { ConsensusExecutionManager } from '../modules/chat-messages/managers/con
import type { ParallelModelTarget } from '../modules/chat-messages/types/parallel.types';
import type { AssembledContext } from '../modules/chat-messages/types/context.types';
import { createFakePaygAccessControl } from '../modules/chat-messages/__tests__/helpers/fake-payg-access-control.helper';
import {
disabledCrossThreadResult,
emptyConversationManifest,
fallbackModelTokenBudget,
} from '../modules/chat-messages/utilities/assembled-context.utility';

jest.spyOn(AppConfig, 'get').mockReturnValue({
CHAT_DATABASE_URL: 'postgresql://test:test@localhost:5432/test',
Expand Down Expand Up @@ -58,6 +63,9 @@ describe('ConsensusExecutionManager', () => {
fileContents: [],
workspaceCitations: [],
tokenBudget: 4096,
modelBudget: fallbackModelTokenBudget(),
conversationManifest: emptyConversationManifest(),
crossThread: disabledCrossThreadResult(),
researchEvidence: [],
researchRunId: null,
researchWarnings: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { EscalationChainManager } from '../modules/chat-messages/managers/escala
import { EscalationChainStatus } from '../common/enums/escalation-chain-status.enum';
import type { EscalationChainStep } from '../modules/chat-messages/types/escalation-chain.types';
import type { AssembledContext } from '../modules/chat-messages/types/context.types';
import {
disabledCrossThreadResult,
emptyConversationManifest,
fallbackModelTokenBudget,
} from '../modules/chat-messages/utilities/assembled-context.utility';

jest.spyOn(AppConfig, 'get').mockReturnValue({
CHAT_DATABASE_URL: 'postgresql://test:test@localhost:5432/test',
Expand Down Expand Up @@ -62,6 +67,9 @@ describe('EscalationChainManager', () => {
fileContents: [],
workspaceCitations: [],
tokenBudget: 4096,
modelBudget: fallbackModelTokenBudget(),
conversationManifest: emptyConversationManifest(),
crossThread: disabledCrossThreadResult(),
researchEvidence: [],
researchRunId: null,
researchWarnings: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import type {
ParallelModelTarget,
} from '../modules/chat-messages/types/parallel.types';
import type { AssembledContext } from '../modules/chat-messages/types/context.types';
import {
disabledCrossThreadResult,
emptyConversationManifest,
fallbackModelTokenBudget,
} from '../modules/chat-messages/utilities/assembled-context.utility';

jest.spyOn(AppConfig, 'get').mockReturnValue({
CHAT_DATABASE_URL: 'postgresql://test:test@localhost:5432/test',
Expand Down Expand Up @@ -97,6 +102,9 @@ describe('ParallelExecutionManager', () => {
fileContents: [],
workspaceCitations: [],
tokenBudget: 4096,
modelBudget: fallbackModelTokenBudget(),
conversationManifest: emptyConversationManifest(),
crossThread: disabledCrossThreadResult(),
researchEvidence: [],
researchRunId: null,
researchWarnings: [],
Expand Down
4 changes: 4 additions & 0 deletions apps/claw-chat-service/src/app/config/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const appConfigSchema = z.object({
OLLAMA_SERVICE_URL: z.string().min(1).default('http://ollama-service:4008'),
LLAMACPP_SERVICE_URL: z.string().min(1).default('http://llamacpp-service:4017'),
CONNECTOR_SERVICE_URL: z.string().min(1).default('http://connector-service:4003'),
// Read for one thing only: the selected model's real context window, which
// chat-service must never keep its own copy of. See ModelContextWindowClient
// and ADR-086.
ROUTING_SERVICE_URL: z.string().min(1).default('https://routing-service:4004'),
MEMORY_SERVICE_URL: z.string().min(1).default('http://memory-service:4005'),
FILE_SERVICE_URL: z.string().min(1).default('http://file-service:4006'),
IMAGE_SERVICE_URL: z.string().min(1).default('http://image-service:4012'),
Expand Down
20 changes: 20 additions & 0 deletions apps/claw-chat-service/src/common/constants/execution.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,28 @@ export const IMAGE_PROVIDER_PREFIX = 'IMAGE_';

export const FILE_GENERATION_PROVIDER = 'FILE_GENERATION';

/**
* DEPRECATED as a context rule. Kept only as the Runtime V2 tool-trail window.
*
* This was the whole conversational memory of the product: `assemble` sliced
* the thread to the last twenty messages and everything downstream cut further.
* Conversational history is now selected by ContextComposerManager against a
* real token budget — see ADR-086. Do not reintroduce a message-count rule.
*/
export const THREAD_CONTEXT_LIMIT = 20;

/**
* How many rows are read from the database for one generation.
*
* Not a context rule — a read cap. The composer decides what of this reaches
* the model. It was 20, applied at the query, so a hundred-message thread had
* eighty messages that no amount of budget could recover: they were never
* loaded. Four hundred rows of a single indexed thread is a cheap read and
* covers a very long conversation; beyond it, hierarchical summaries (Batch 2)
* take over rather than an ever-larger SELECT.
*/
export const THREAD_HISTORY_FETCH_LIMIT = 400;

export const MEMORY_FETCH_LIMIT = 20;

/**
Expand Down
1 change: 1 addition & 0 deletions apps/claw-chat-service/src/common/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export {
OLLAMA_PROVIDER,
PROVIDER_BASE_URLS,
THREAD_CONTEXT_LIMIT,
THREAD_HISTORY_FETCH_LIMIT,
VIDEO_MIME_PREFIX,
WORKSPACE_CONTEXT_LIMIT,
} from './execution.constants';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type MemorySensitivity,
type MemoryType,
type RetrievalBundle,
type RetrievalConversationSummary,
RetrievalReason,
} from '@claw/shared-types';
import type { AssembledContext } from '../../modules/chat-messages/types/context.types';
Expand Down Expand Up @@ -45,12 +46,59 @@ export function receiptFromAssembledContext(
memories,
packItems,
assemblyOrder: [
...(context.conversationManifest?.includedMessageIds ?? []).map((id) => `message:${id}`),
...(context.crossThread?.selections ?? []).map(
(selection) => `prior-message:${selection.messageId}`,
),
...memories.map((m) => `memory:${m.id}`),
...packItems.map((p) => `pack:${p.id}`),
],
tokenBudget: context.tokenBudget,
tokenBudgetUsed,
retrievalLatencyMs: 0,
warnings: [],
retrievalLatencyMs: context.conversationManifest?.retrievalMs ?? 0,
warnings: context.conversationManifest?.warnings ?? [],
conversation: conversationSummary(context),
};
}

/**
* The conversational half of the receipt.
*
* This is the part that answers "why did the AI forget this?". Everything here
* is derived from the composer's own decision record, not recomputed, so the
* receipt cannot disagree with what was actually sent.
*/
function conversationSummary(context: AssembledContext): RetrievalConversationSummary | undefined {
// A receipt is a diagnostic artifact. It must never be the reason a
// generation fails, so a context built by a path that predates the manifest
// yields no conversation summary rather than a thrown TypeError.
const manifest = context.conversationManifest;
if (manifest === undefined || manifest === null) {
return undefined;
}
const omissionReasons: Record<string, string> = {};
for (const omitted of manifest.omitted) {
omissionReasons[omitted.messageId] = omitted.reason;
}
return {
totalThreadMessages: manifest.totalThreadMessages,
includedMessageIds: manifest.includedMessageIds,
includedTurnCount: manifest.includedTurnCount,
omittedMessageIds: manifest.omitted.map((omitted) => omitted.messageId),
omissionReasons,
estimatedInputTokens: manifest.estimatedInputTokens,
contextWindowTokens: manifest.budget.contextWindowTokens,
reservedOutputTokens: manifest.budget.reservedOutputTokens,
availableInputTokens: manifest.budget.availableInputTokens,
contextWindowSource: manifest.budget.source,
referenceSignals: manifest.referenceSignal.signals,
priorThreadsSearched: context.crossThread?.searchedThreadIds ?? [],
priorThreadsUsed: context.crossThread?.usedThreadIds ?? [],
priorMessageIds: (context.crossThread?.selections ?? []).map(
(selection) => selection.messageId,
),
crossThreadSkipReason: context.crossThread?.skippedReason ?? null,
retrievalMs: manifest.retrievalMs,
selectionMs: manifest.selectionMs,
};
}
Loading
Loading