Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
14ccef8
guard model context before launch
Siri-Ray Jul 17, 2026
cf95984
fix(daemon): recognize dotted Claude model IDs
Siri-Ray Jul 21, 2026
4c9682f
fix(daemon): budget Claude model aliases
Siri-Ray Jul 21, 2026
394cbad
fix(daemon): enrich AMR limits with live prices
Siri-Ray Jul 21, 2026
43b6403
roll over near-limit agent sessions (#5816)
Siri-Ray Jul 21, 2026
0723666
fix(daemon): close context rollover budget gaps
Siri-Ray Jul 21, 2026
e47bab4
fix(daemon): budget rollover transcript against prompt headroom
Siri-Ray Jul 21, 2026
db41a28
fix(daemon): merge main into model context budget
Siri-Ray Jul 22, 2026
c36dd47
fix(daemon): preserve context budget smoke coverage after main merge
Siri-Ray Jul 22, 2026
feb4cc3
test(web): synchronize question form loading
Siri-Ray Jul 22, 2026
3f9d14d
merge: resolve model context budget conflict with main
Siri-Ray Jul 24, 2026
336125d
merge: resolve context budget conflict with Codex preflight
Siri-Ray Jul 24, 2026
a81037b
merge: stabilize rollover validation with current main
Siri-Ray Jul 29, 2026
ca60238
test(web): stabilize privacy consent click
Siri-Ray Jul 29, 2026
ed8ef95
merge: reconcile context budgeting with current main
Siri-Ray Jul 30, 2026
35b8497
merge: update model-context-budget branch with main
Siri-Ray Aug 6, 2026
1ca4dfe
merge: reconcile context budget analytics with main
Siri-Ray Aug 10, 2026
13d780b
merge: preserve context budget fields with run terminal telemetry
Siri-Ray Aug 10, 2026
a321de7
merge: reconcile context budget and publish analytics
Siri-Ray Aug 11, 2026
11d8104
merge: reconcile context rollover with current main
Siri-Ray Aug 11, 2026
2133fec
merge: reconcile context telemetry with append-only persistence
Siri-Ray Aug 17, 2026
4583df9
test(daemon): extend rollover test timeout
Siri-Ray Aug 18, 2026
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
40 changes: 40 additions & 0 deletions apps/daemon/src/agent-session-resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export interface AgentResumeContext {
isResuming: boolean;
/** Hash of the stable instruction block last sent on this session, or null. */
storedStablePromptHash: string | null;
/** Effective provider input size recorded on the session's last turn. */
storedInputTokens: number | null;
/**
* Per-section digests behind `storedStablePromptHash`, for naming which input
* drifted when the hash no longer matches. Diagnostic only — never an input
Expand All @@ -39,6 +41,41 @@ export interface AgentResumeContext {
invalidationReason: ResumeInvalidationReason | null;
}

function readStoredSessionInputTokens(
db: SqliteDb,
messageId: string | null | undefined,
): number | null {
if (!messageId) return null;
const row = db
.prepare('SELECT events_json AS eventsJson FROM messages WHERE id = ?')
.get(messageId) as { eventsJson?: unknown } | undefined;
if (!row || typeof row.eventsJson !== 'string') return null;
let events: unknown;
try {
events = JSON.parse(row.eventsJson);
} catch {
return null;
}
if (!Array.isArray(events)) return null;
for (let index = events.length - 1; index >= 0; index -= 1) {
const event = events[index];
if (!event || typeof event !== 'object' || Array.isArray(event)) continue;
const usage = event as {
kind?: unknown;
inputTokens?: unknown;
inputTokensEffective?: unknown;
};
if (usage.kind !== 'usage') continue;
const effective = typeof usage.inputTokensEffective === 'number'
? usage.inputTokensEffective
: usage.inputTokens;
if (typeof effective === 'number' && Number.isFinite(effective) && effective > 0) {
return Math.floor(effective);
}
}
return null;
}

export type CapturedAgentSessionResult = 'stored' | 'cleared' | 'skipped';

/**
Expand Down Expand Up @@ -122,6 +159,9 @@ export function resolveAgentResumeContext(
newSessionId: randomUUID(),
isResuming: resumable,
storedStablePromptHash: resumable ? (record?.stablePromptHash ?? null) : null,
storedInputTokens: resumable
? readStoredSessionInputTokens(db, record?.lastMessageId)
: null,
storedStableSections: resumable ? parseStableSections(record?.stablePromptSections) : null,
invalidationReason,
};
Expand Down
28 changes: 25 additions & 3 deletions apps/daemon/src/integrations/provider-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ function openAiModelOption(item: unknown): ProviderModelOption | null {
const obj = item as { id?: unknown; metadata?: unknown };
const id = typeof obj?.id === 'string' ? obj.id : '';
if (!id || !isOpenAiChatModelId(id)) return null;
const metadata = extractModelMetadata(obj.metadata);
const metadata = extractModelMetadata(obj);
return {
id,
label: id,
Expand All @@ -151,16 +151,38 @@ function openAiModelOption(item: unknown): ProviderModelOption | null {

function extractModelMetadata(value: unknown): ModelMetadata | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
const metadata = value as { cost?: unknown; capability?: unknown };
const record = value as Record<string, unknown>;
const metadata = (
record.metadata && typeof record.metadata === 'object' && !Array.isArray(record.metadata)
? record.metadata
: record
) as Record<string, unknown>;
const cost = parseModelCost(metadata.cost);
const capability = parseModelCapability(metadata.capability);
if (!cost && !capability) return null;
const contextWindowTokens = positiveInteger(
metadata.contextWindowTokens ??
metadata.context_window_tokens ??
record.context_length ??
record.contextWindow,
);
const maxOutputTokens = positiveInteger(
metadata.maxOutputTokens ?? metadata.max_output_tokens ?? record.max_output_tokens,
);
if (!cost && !capability && !contextWindowTokens && !maxOutputTokens) return null;
return {
...(cost ? { cost } : {}),
...(capability ? { capability } : {}),
...(contextWindowTokens ? { contextWindowTokens } : {}),
...(maxOutputTokens ? { maxOutputTokens } : {}),
};
}

function positiveInteger(value: unknown): number | null {
return typeof value === 'number' && Number.isInteger(value) && value > 0
? value
: null;
}

function parseModelCost(value: unknown): ModelCost | null {
return value === 'low' ||
value === 'medium' ||
Expand Down
15 changes: 15 additions & 0 deletions apps/daemon/src/langfuse-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ interface DaemonRunRecord {
promptTelemetry?: PromptStackTelemetry;
projectAttachmentPaths?: string[];
projectMetadata?: Record<string, unknown> | null;
contextBudget?: {
action: string;
source: string;
estimatedPromptTokens: number;
contextWindowTokens?: number;
reservedOutputTokens?: number;
inputBudgetTokens?: number;
budgetRatio?: number;
priorSessionInputTokens?: number;
projectedInputTokens?: number;
rolloverThresholdTokens?: number;
compactedPromptTokens?: number;
omittedTranscriptMessageBlocks?: number;
};
retryAttemptCount?: number;
retryFinalResult?: string;
retrySuppressedReason?: string;
Expand Down Expand Up @@ -1103,6 +1117,7 @@ export async function reportRunCompletedFromDaemon(
...(stderr ? { stderr } : {}),
...(stdout ? { stdout } : {}),
diagnostics,
...(run.contextBudget ? { contextBudget: run.contextBudget } : {}),
retryAttemptCount: run.retryAttemptCount ?? 0,
...(run.retryFinalResult
? { retryFinalResult: run.retryFinalResult }
Expand Down
26 changes: 26 additions & 0 deletions apps/daemon/src/langfuse-trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,20 @@ export interface RunSummary {
truncated: boolean;
};
diagnostics?: unknown;
contextBudget?: {
action: string;
source: string;
estimatedPromptTokens: number;
contextWindowTokens?: number;
reservedOutputTokens?: number;
inputBudgetTokens?: number;
budgetRatio?: number;
priorSessionInputTokens?: number;
projectedInputTokens?: number;
rolloverThresholdTokens?: number;
compactedPromptTokens?: number;
omittedTranscriptMessageBlocks?: number;
};
retryAttemptCount?: number;
retryFinalResult?: string;
retrySuppressedReason?: string;
Expand Down Expand Up @@ -1468,6 +1482,18 @@ export function buildTracePayload(ctx: ReportContext): unknown[] {
stderr: ctx.run.stderr,
stdout: ctx.run.stdout,
diagnostics: ctx.run.diagnostics,
contextBudgetAction: ctx.run.contextBudget?.action,
contextBudgetSource: ctx.run.contextBudget?.source,
estimatedPromptTokens: ctx.run.contextBudget?.estimatedPromptTokens,
contextWindowTokens: ctx.run.contextBudget?.contextWindowTokens,
reservedOutputTokens: ctx.run.contextBudget?.reservedOutputTokens,
inputBudgetTokens: ctx.run.contextBudget?.inputBudgetTokens,
contextBudgetRatio: ctx.run.contextBudget?.budgetRatio,
priorSessionInputTokens: ctx.run.contextBudget?.priorSessionInputTokens,
projectedSessionInputTokens: ctx.run.contextBudget?.projectedInputTokens,
rolloverThresholdTokens: ctx.run.contextBudget?.rolloverThresholdTokens,
compactedPromptTokens: ctx.run.contextBudget?.compactedPromptTokens,
omittedTranscriptMessageBlocks: ctx.run.contextBudget?.omittedTranscriptMessageBlocks,
eventsSummary: ctx.eventsSummary,
tokens,
cost_usd: costBreakdown.cost_usd,
Expand Down
47 changes: 47 additions & 0 deletions apps/daemon/src/routes/runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,21 @@ interface ChatRun {
retryAttemptCount?: number;
retryFinalResult?: string;
retrySuppressedReason?: string;
contextBudget?: {
action: 'unmeasured' | 'within_budget' | 'blocked' | 'rollover';
source: 'model_metadata' | 'known_model_family' | 'unknown';
estimatedPromptTokens: number;
contextWindowTokens?: number;
reservedOutputTokens?: number;
safetyMarginTokens?: number;
inputBudgetTokens?: number;
budgetRatio?: number;
priorSessionInputTokens?: number;
projectedInputTokens?: number;
rolloverThresholdTokens?: number;
compactedPromptTokens?: number;
omittedTranscriptMessageBlocks?: number;
};
retryOriginalFailure?: {
failure_category?: string;
failure_detail?: string;
Expand Down Expand Up @@ -1294,6 +1309,38 @@ export function registerRunRoutes(app: Express, ctx: RegisterRunRoutesDeps) {
asked_user_question: runAskedUserQuestion(run.events),
retry_attempt_count: run.retryAttemptCount ?? 0,
retry_final_result: run.retryFinalResult ?? 'not_attempted',
context_budget_action: run.contextBudget?.action ?? 'unmeasured',
context_budget_source: run.contextBudget?.source ?? 'unknown',
...(run.contextBudget?.estimatedPromptTokens !== undefined
? { estimated_prompt_tokens: run.contextBudget.estimatedPromptTokens }
: {}),
...(run.contextBudget?.contextWindowTokens !== undefined
? { context_window_tokens: run.contextBudget.contextWindowTokens }
: {}),
...(run.contextBudget?.reservedOutputTokens !== undefined
? { reserved_output_tokens: run.contextBudget.reservedOutputTokens }
: {}),
...(run.contextBudget?.inputBudgetTokens !== undefined
? { input_budget_tokens: run.contextBudget.inputBudgetTokens }
: {}),
...(run.contextBudget?.budgetRatio !== undefined
? { context_budget_ratio: run.contextBudget.budgetRatio }
: {}),
...(run.contextBudget?.priorSessionInputTokens !== undefined
? { prior_session_input_tokens: run.contextBudget.priorSessionInputTokens }
: {}),
...(run.contextBudget?.projectedInputTokens !== undefined
? { projected_session_input_tokens: run.contextBudget.projectedInputTokens }
: {}),
...(run.contextBudget?.rolloverThresholdTokens !== undefined
? { rollover_threshold_tokens: run.contextBudget.rolloverThresholdTokens }
: {}),
...(run.contextBudget?.compactedPromptTokens !== undefined
? { compacted_prompt_tokens: run.contextBudget.compactedPromptTokens }
: {}),
...(run.contextBudget?.omittedTranscriptMessageBlocks !== undefined
? { omitted_transcript_message_blocks: run.contextBudget.omittedTranscriptMessageBlocks }
: {}),
...(agentCliVersion
? { agent_cli_version: agentCliVersion }
: {}),
Expand Down
9 changes: 9 additions & 0 deletions apps/daemon/src/runtimes/chat-run-messages.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type Database from 'better-sqlite3';
import type { PersistedAgentEvent } from '@open-design/contracts';
import { scanRunEventsForUsageAnalytics } from '../run-analytics-observability.js';
import {
appendMessageAgentEvent,
upsertMessage,
Expand Down Expand Up @@ -224,9 +225,17 @@ export function daemonAgentPayloadToPersistedAgentEvent(data: unknown): Persiste
}
if (type === 'usage') {
const usage = isRecord(data.usage) ? data.usage : {};
const usageAnalytics = scanRunEventsForUsageAnalytics(
[{ event: 'agent', data }],
null,
0,
);
return {
kind: 'usage',
...(typeof usage.input_tokens === 'number' ? { inputTokens: usage.input_tokens } : {}),
...(typeof usageAnalytics.input_tokens_effective === 'number'
? { inputTokensEffective: usageAnalytics.input_tokens_effective }
: {}),
...(typeof usage.output_tokens === 'number' ? { outputTokens: usage.output_tokens } : {}),
...(typeof data.costUsd === 'number' ? { costUsd: data.costUsd } : {}),
...(typeof data.durationMs === 'number' ? { durationMs: data.durationMs } : {}),
Expand Down
55 changes: 47 additions & 8 deletions apps/daemon/src/runtimes/defs/amr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,45 @@ function extractModelMetadata(item: unknown): ModelMetadata | null {
const metadata = isRecord(item.metadata) ? item.metadata : item;
const cost = parseModelCost(metadata.cost);
const capability = parseModelCapability(metadata.capability);
if (!cost && !capability) return null;
const contextWindowTokens = extractPositiveModelLimit(item, [
'contextWindowTokens',
'context_window_tokens',
'context_length',
'contextLength',
'context',
]);
const maxOutputTokens = extractPositiveModelLimit(item, [
'maxOutputTokens',
'max_output_tokens',
'output',
]);
if (!cost && !capability && !contextWindowTokens && !maxOutputTokens) return null;
return {
...(cost ? { cost } : {}),
...(capability ? { capability } : {}),
...(contextWindowTokens ? { contextWindowTokens } : {}),
...(maxOutputTokens ? { maxOutputTokens } : {}),
};
}

function extractPositiveModelLimit(
item: Record<string, unknown>,
keys: string[],
): number | null {
const metadata = isRecord(item.metadata) ? item.metadata : null;
const limit = isRecord(item.limit) ? item.limit : null;
for (const source of [metadata, limit, item]) {
if (!source) continue;
for (const key of keys) {
const value = source[key];
if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
return value;
}
}
}
return null;
}

function withPriceDerivedCostMetadata(
metadata: ModelMetadata | null,
inputPriceUsdPerMillion: number | undefined,
Expand Down Expand Up @@ -337,22 +369,23 @@ function enrichVelaModelsFromOpenCodeCatalog(
models: RuntimeModelOption[],
env: NodeJS.ProcessEnv,
): RuntimeModelOption[] {
if (models.every((model) => model.inputPriceUsdPerMillion !== undefined)) {
return models;
}
const catalog = readOpenCodeModelCatalog(env);
if (!catalog) return models;
return models.map((model) => {
if (model.inputPriceUsdPerMillion !== undefined) return model;
const price = lookupOpenCodeModelPrice(catalog, model.id);
if (!price) return model;
const metadata = {
...(price.metadata ?? {}),
...(model.metadata ?? {}),
};
const inputPriceUsdPerMillion =
model.inputPriceUsdPerMillion ?? price.inputPriceUsdPerMillion;
const outputPriceUsdPerMillion =
model.outputPriceUsdPerMillion ?? price.outputPriceUsdPerMillion;
return {
...model,
...price,
...(inputPriceUsdPerMillion === undefined ? {} : { inputPriceUsdPerMillion }),
...(outputPriceUsdPerMillion === undefined ? {} : { outputPriceUsdPerMillion }),
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
};
});
Expand Down Expand Up @@ -462,14 +495,20 @@ function openCodeModelPrice(
> | null {
if (!isRecord(model)) return null;
const inputPriceUsdPerMillion = extractInputPriceUsdPerMillion(model);
if (inputPriceUsdPerMillion === undefined) return null;
const outputPriceUsdPerMillion = extractOutputPriceUsdPerMillion(model);
const metadata = withPriceDerivedCostMetadata(
extractModelMetadata(model),
inputPriceUsdPerMillion,
);
if (
inputPriceUsdPerMillion === undefined &&
outputPriceUsdPerMillion === undefined &&
metadata === null
) {
return null;
}
return {
inputPriceUsdPerMillion,
...(inputPriceUsdPerMillion === undefined ? {} : { inputPriceUsdPerMillion }),
Comment thread
Siri-Ray marked this conversation as resolved.
...(outputPriceUsdPerMillion === undefined ? {} : { outputPriceUsdPerMillion }),
...(metadata === null ? {} : { metadata }),
};
Expand Down
Loading
Loading