Skip to content
43 changes: 23 additions & 20 deletions apps/desktop/src/main/session-stream.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto';
import type { SessionChangedReason, SessionEvent } from '@maka/core';
import type { LlmCallRecord, ToolInvocationRecord } from '@maka/core/usage-stats/types';
import type { ToolInvocationRecord } from '@maka/core/usage-stats/types';
import {
AiSdkBackend,
buildDefaultContextBudgetPolicy,
Expand All @@ -11,7 +11,6 @@ import {
loadHistoryCompactBlocksFromArtifacts,
loadSynthesisCacheBlocksFromArtifacts,
persistSynthesisCacheBlocksToArtifacts,
recordLlmCall,
recordToolInvocation,
renderPlanExecutionPrompt,
renderInterruptedPlanContext,
Expand Down Expand Up @@ -85,8 +84,8 @@ export interface AiSdkBackendFactoryDeps extends DesktopBackendToolSurfaceDeps {
* seams that resolve AFTER the registration point are injected as accessors:
* `getRuntime` (the SessionManager is constructed after registration) and
* `getLookupPricing` (a mutable pricing lookup reassigned by usage IPC + startup;
* read live per `recordLlmCall`, snapshotted once for the `lookupPricing` field —
* matching the original module-`let` closure semantics exactly).
* snapshotted once for the `lookupPricing` field — matching the original
* module-`let` closure semantics exactly).
*/
export function createAiSdkBackendFactory(deps: AiSdkBackendFactoryDeps): BackendFactory {
const {
Expand Down Expand Up @@ -136,6 +135,24 @@ export function createAiSdkBackendFactory(deps: AiSdkBackendFactoryDeps): Backen
mode: effectivePermissionMode,
cwd: ctx.header.cwd,
});
// Hoisted out of the backend input so the shape stays readable; the
// auxiliary summarizer no longer needs any of it (#1679).
const providerRequestCapture = ctx.recordProviderRequestCapture
? createProviderRequestCaptureRecorder({
persistArtifact: async (capture) => {
const artifact = await persistProviderRequestCaptureArtifact(artifactStore, {
sessionId: ctx.sessionId,
turnId: capture.turnId,
captureId: capture.captureId,
step: capture.step,
serializedRequest: capture.serializedRequest,
now: Date.now(),
});
return { artifactId: artifact.id };
},
recordLedger: ctx.recordProviderRequestCapture,
})
: undefined;

return new AiSdkBackend({
sessionId: ctx.sessionId,
Expand Down Expand Up @@ -281,7 +298,6 @@ export function createAiSdkBackendFactory(deps: AiSdkBackendFactoryDeps): Backen
},
shellRunContextSummary: ctx.shellRunContextSummary,
lookupPricing: getLookupPricing(),
recordLlmCall: (event: LlmCallRecord) => recordLlmCall({ repo: telemetryRepo, lookupPricing: getLookupPricing() }, event),
// One canonical record, one commit point (#1679): the AgentRun stream is
// the only durable authority, and the ledger is a projection written only
// after the authority holds the record. A failed projection marks the run
Expand Down Expand Up @@ -340,22 +356,9 @@ export function createAiSdkBackendFactory(deps: AiSdkBackendFactoryDeps): Backen
},
}),
recordRunTrace: ctx.recordRunTrace,
...(ctx.recordProviderRequestCapture
...(providerRequestCapture
? {
recordProviderRequestCapture: createProviderRequestCaptureRecorder({
persistArtifact: async (capture) => {
const artifact = await persistProviderRequestCaptureArtifact(artifactStore, {
sessionId: ctx.sessionId,
turnId: capture.turnId,
captureId: capture.captureId,
step: capture.step,
serializedRequest: capture.serializedRequest,
now: Date.now(),
});
return { artifactId: artifact.id };
},
recordLedger: ctx.recordProviderRequestCapture,
}),
recordProviderRequestCapture: providerRequestCapture,
recordProviderRequestAttempt: ctx.recordProviderRequestAttempt,
}
: {}),
Expand Down
8 changes: 5 additions & 3 deletions apps/desktop/src/main/usage-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ const USAGE_REPAIR_RUNS_PER_QUERY = 16;
export function registerUsageIpc(deps: UsageIpcDeps): void {
/**
* Usage answers sum two sources (#1679): the canonical model-call ledger and
* the frozen `LlmCallRecord` table, which still receives the compaction calls
* that have not been routed through the canonical seam yet. Every merged
* result carries the provenance that qualifies it.
* the frozen `LlmCallRecord` table. Both compaction kinds now settle through
* the canonical seam; what still lands in the frozen table is historical rows
* and `goal_evaluation`, the one kind the seam cannot yet identify (it has no
* run or turn at the Host layer). Every merged result carries the provenance
* that qualifies it.
*/
const canonicalUsage = async (query: UsageQuery, now: number): Promise<CanonicalUsageSource> => {
// Fold in whatever the authority holds and this read model is behind on
Expand Down
82 changes: 82 additions & 0 deletions packages/cli/src/__tests__/runtime-bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { access, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, test } from 'node:test';
import {
MODEL_CALL_ATTEMPT_SCHEMA_VERSION,
type ModelCallAttempt,
} from '@maka/core/model-call-attempt';
import {
createConnectionStore,
createFileCredentialStore,
Expand Down Expand Up @@ -38,6 +42,29 @@ import {
resolveCliStreamConnectTimeoutMs,
} from '../runtime-bootstrap.js';

function modelCallAttemptFixture(): ModelCallAttempt {
return {
schemaVersion: MODEL_CALL_ATTEMPT_SCHEMA_VERSION,
logicalCallId: 'call-1',
attemptId: 'attempt-1',
traceId: 'trace-1',
sessionId: 'session-1',
runId: 'run-1',
turnId: 'turn-1',
step: 0,
attempt: 0,
callKind: 'history_compact',
providerId: 'ollama',
modelId: 'llama3.2',
startedAt: 1,
completedAt: 2,
latencyMs: 1,
status: 'completed',
usageBasis: 'missing',
costBasis: 'unpriced',
};
}

describe('Maka CLI runtime bootstrap', () => {
test('parses the CLI stream connect timeout override', () => {
assert.equal(resolveCliStreamConnectTimeoutMs({}), undefined);
Expand Down Expand Up @@ -205,6 +232,61 @@ describe('Maka CLI runtime bootstrap', () => {
});
});

test('forwards the canonical metering sink from the backend context', async () => {
// The CLI factory used to wire capture and attempt diagnostics but not the
// canonical sink, so `/compact` and ordinary sends produced no
// `ModelCallAttempt` at all — the kernel offered one and nothing took it.
await withWorkspace(async (workspaceRoot) => {
const connectionStore = createConnectionStore(workspaceRoot);
await connectionStore.create({
slug: 'local',
name: 'Local Ollama',
providerType: 'ollama',
defaultModel: 'llama3.2',
});
const context = await createMakaCliRuntimeContext({
surface: 'tui',
workspaceRoot,
cwd: '/repo',
});
try {
const session = await context.runtime.createSession({
cwd: context.cwd,
backend: 'ai-sdk',
llmConnectionSlug: context.target.connection.slug,
model: context.target.model,
permissionMode: 'explore',
name: 'metering-sink',
});
const runtimeDeps = (context.runtime as unknown as RuntimeWithPrivateDeps).deps;
const header = await runtimeDeps.store.readHeader(session.id);
const recorded: ModelCallAttempt[] = [];
const backend = await runtimeDeps.backends.build('ai-sdk', {
sessionId: session.id,
workspaceRoot,
header,
store: runtimeDeps.store,
recordModelCallAttempt: (attempt: ModelCallAttempt) => {
recorded.push(attempt);
return Promise.resolve();
},
});
const backendInput = (backend as unknown as { input: AiSdkBackendInput }).input;

assert.equal(
typeof backendInput.recordModelCallAttempt,
'function',
'the composition must pass the sink the kernel offers',
);
await backendInput.recordModelCallAttempt?.(modelCallAttemptFixture());
assert.equal(recorded.length, 1, 'and it must reach the context, not a local stub');
assert.equal(recorded[0]?.callKind, 'history_compact');
} finally {
await context.close();
}
});
});

test('uses an explicit connection and forwards one-shot limits and invocation results', async () => {
await withWorkspace(async (workspaceRoot) => {
const connectionStore = createConnectionStore(workspaceRoot);
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/runtime-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,10 @@ export async function createMakaCliRuntimeContext(
}),
providerOptions: buildProviderOptions(ready.connection, ready.model, header.thinkingLevel),
}),
// The canonical metering sink (#1679). Without it this composition root
// produces diagnostics and no accounting at all — for `/compact` and for
// ordinary sends alike.
...(ctx.recordModelCallAttempt ? { recordModelCallAttempt: ctx.recordModelCallAttempt } : {}),
recordHistoryCompactCheckpoint: ctx.recordHistoryCompactCheckpoint,
loadTurnRuntimeEvents: ctx.loadTurnRuntimeEvents,
allowMidTurnHistoryCompaction: ctx.allowMidTurnHistoryCompaction,
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/backend-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ export interface SteeringLease {

export interface BackendCompactHistoryInput {
turnId: string;
/**
* The run this compaction belongs to. Required, not optional: a manual
* compaction is a real model call, and a call whose run cannot be named is a
* call nothing can bill (#1679). Unlike `send`, this path has no turn state to
* infer it from, so the caller that opened the run states it.
*/
runId: string;
runtimeContext: readonly RuntimeEvent[];
/** Override the configured recent-turn tail for an explicit recovery compaction. */
minRecentTurns?: number;
Expand Down
75 changes: 75 additions & 0 deletions packages/headless/src/__tests__/harbor-cell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import type {
import type { BackendSendInput, BackendStopMode } from '@maka/core/backend-types';
import type { SandboxBoundaryResponse } from '@maka/core/sandbox-boundary';
import { createSessionStore } from '@maka/storage';
import {
MODEL_CALL_ATTEMPT_SCHEMA_VERSION,
type ModelCallAttempt,
} from '@maka/core/model-call-attempt';
import {
BackendRegistry,
PiAgentBackend,
Expand Down Expand Up @@ -1985,6 +1989,54 @@ describe('runHarborCell', () => {
});
});

test('Harbor ai-sdk backend registration forwards the canonical metering sink', async () => {
// The controller has always exposed `recordModelCallAttempt`; this
// composition never passed it on, so Harbor produced diagnostic attempts
// with no canonical record behind them (#1679).
await withDirs(async ({ workspaceDir, artifactStore }) => {
const registry = new BackendRegistry();
const toolExecutor = fakeToolExecutor();
const register = buildAiSdkCellBackendRegistration({
provider: 'openai',
model: 'gpt-5.6-sol',
env: { OPENAI_API_KEY: 'test-key' },
now: () => 123,
newId: () => 'id',
});
await registerProjectedAiSdkBackend(register, registry, {
config: {
id: 'harbor-ai-sdk',
backend: 'ai-sdk',
llmConnectionSlug: 'openai',
model: 'gpt-5.6-sol',
systemPrompt: DEFAULT_HEADLESS_SYSTEM_PROMPT,
},
task: { id: 'harbor-cell', instruction: 'solve', workspaceDir },
storageRoot: workspaceDir,
workspaceDir,
artifactStore,
realBackendIsolation: { kind: 'external', label: 'Harbor task container', toolExecutor },
toolExecutor,
...createHeadlessSessionCapabilityBridge().capabilities,
});

const recorded: ModelCallAttempt[] = [];
const backend = await registry.build('ai-sdk', {
...backendContext(workspaceDir),
recordModelCallAttempt: (attempt: ModelCallAttempt) => {
recorded.push(attempt);
return Promise.resolve();
},
});
const backendInput = (backend as unknown as { input: AiSdkBackendInput }).input;

assert.equal(typeof backendInput.recordModelCallAttempt, 'function');
await backendInput.recordModelCallAttempt?.(harborModelCallAttemptFixture());
assert.equal(recorded.length, 1, 'the sink must reach the controller');
assert.equal(recorded[0]?.callKind, 'semantic_compact');
});
});

test('Harbor ai-sdk backend registration exposes native file tools to the provider schema', async () => {
await withDirs(async ({ workspaceDir, artifactStore }) => {
const registry = new BackendRegistry();
Expand Down Expand Up @@ -4780,6 +4832,29 @@ function sha256(text: string): string {
return createHash('sha256').update(text).digest('hex');
}

function harborModelCallAttemptFixture(): ModelCallAttempt {
return {
schemaVersion: MODEL_CALL_ATTEMPT_SCHEMA_VERSION,
logicalCallId: 'call-1',
attemptId: 'attempt-1',
traceId: 'trace-1',
sessionId: 'session-1',
runId: 'run-1',
turnId: 'turn-1',
step: 0,
attempt: 0,
callKind: 'semantic_compact',
providerId: 'openai',
modelId: 'gpt-5.6-sol',
startedAt: 1,
completedAt: 2,
latencyMs: 1,
status: 'completed',
usageBasis: 'missing',
costBasis: 'unpriced',
};
}

function backendContext(workspaceDir: string): BackendFactoryContext {
return {
sessionId: 'session-1',
Expand Down
5 changes: 5 additions & 0 deletions packages/headless/src/harbor-cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,11 @@ export function buildAiSdkCellBackendRegistration(input: {
newId: input.newId,
now: input.now,
recordRunTrace: ctx.recordRunTrace,
// The canonical metering sink (#1679); the controller has always
// exposed it, this composition just never passed it through.
...(ctx.recordModelCallAttempt
? { recordModelCallAttempt: ctx.recordModelCallAttempt }
: {}),
...(ctx.recordProviderRequestCapture
? {
recordProviderRequestCapture: createProviderRequestCaptureRecorder({
Expand Down
6 changes: 5 additions & 1 deletion packages/headless/src/provider-request-trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,12 @@ export function assertProviderRequestTraceComplete(
if (!identities.some((candidate) => candidate.turnId === attempt.turnId)) {
fail(`attempt ${attempt.attemptId} has another turn id`);
}
// Capture ids are optional on the record since #1679 — an attempt made in a
// deployment with capture switched off has none. This analysis reads a
// capture ledger, so an attempt that cannot name one is incomplete here;
// the decoder above already rejects such records as invalid_attempt.
const capture =
captures.get(attempt.captureId) ??
(attempt.captureId !== undefined ? captures.get(attempt.captureId) : undefined) ??
fail(`attempt ${attempt.attemptId} does not match its request capture`);
if (!attemptMatchesCapture(attempt, capture)) {
fail(`attempt ${attempt.attemptId} does not match its request capture`);
Expand Down
24 changes: 0 additions & 24 deletions packages/runtime-host/src/server/execution-model-composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,15 +501,10 @@ export async function createHostAiSdkBackend(input: HostAiSdkBackendInput): Prom
}
};
const telemetry = {
insertLlmCall: (record: Parameters<typeof input.usage.telemetry.recordLlmCall>[0]) =>
persistTelemetry(() => input.usage.telemetry.recordLlmCall(record)),
insertToolInvocation: (
record: Parameters<typeof input.usage.telemetry.recordToolInvocation>[0],
) => persistTelemetry(() => input.usage.telemetry.recordToolInvocation(record)),
};
const recordLlmUsage = (event: Parameters<typeof recordLlmCall>[1]) => {
void recordLlmCall({ repo: telemetry, lookupPricing: pricing }, event);
};
/**
* One canonical record, one commit point (#1679).
*
Expand Down Expand Up @@ -637,24 +632,6 @@ export async function createHostAiSdkBackend(input: HostAiSdkBackendInput): Prom
modelId: target.model,
}),
providerOptions,
...(providerRequestCapture
? {
providerRequestTracking: {
now: Date.now,
newId: randomUUID,
persistCapture: providerRequestCapture,
recordAttempt: recordProviderRequestAttempt,
},
}
: {}),
telemetry: {
connectionSlug: target.connection.slug,
providerId: target.connection.providerType,
modelId: target.model,
newId: randomUUID,
now: Date.now,
recordLlmCall: recordLlmUsage,
},
}),
recordHistoryCompactCheckpoint: input.context.recordHistoryCompactCheckpoint,
loadTurnRuntimeEvents: input.context.loadTurnRuntimeEvents,
Expand All @@ -666,7 +643,6 @@ export async function createHostAiSdkBackend(input: HostAiSdkBackendInput): Prom
turnTailPrompt: modelComposition.turnTailPrompt,
shellRunContextSummary: input.context.shellRunContextSummary,
lookupPricing: pricing,
recordLlmCall: recordLlmUsage,
recordModelCallAttempt,
assertModelCallAccountingReady,
recordToolInvocation: (event) => recordToolInvocation({ repo: telemetry }, event),
Expand Down
Loading
Loading