@otto-code/protocol 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/messages.js CHANGED
@@ -46,6 +46,28 @@ const MutableStructuredGenerationProviderSchema = z
46
46
  const MutableMetadataGenerationConfigSchema = z
47
47
  .object({
48
48
  providers: z.array(MutableStructuredGenerationProviderSchema).default([]),
49
+ // Master switch for daemon-side metadata generation (chat auto-titles,
50
+ // agent progress summaries, and other structured side-generations). Default
51
+ // true preserves today's behavior. Read by the generation path (WP-B).
52
+ enabled: z.boolean().default(true),
53
+ // When true, metadata generation prefers a role-matched Writer personality
54
+ // over the cheap default tier. Default false — cheap-tier routing is the
55
+ // default. Read by the generation routing (WP-B).
56
+ preferWriterPersonalities: z.boolean().default(false),
57
+ })
58
+ .passthrough();
59
+ // Daemon-wide agent behavior toggles. Each maps to a Claude-tier capability;
60
+ // providers that can't honor a setting silently ignore it (WP-E wires the
61
+ // reads). All default true so a fresh host behaves exactly like today.
62
+ const MutableAgentBehaviorsConfigSchema = z
63
+ .object({
64
+ // Native next-prompt predictions (Claude prompt_suggestion stream events).
65
+ promptSuggestions: z.boolean().default(true),
66
+ // Agent-authored progress summaries emitted during a turn.
67
+ agentProgressSummaries: z.boolean().default(true),
68
+ // Default value of an agent's notifyOnFinish when the spawn path leaves it
69
+ // unspecified (the current implicit default).
70
+ notifyOnFinishDefault: z.boolean().default(true),
49
71
  })
50
72
  .passthrough();
51
73
  export const TerminalProfileSchema = z
@@ -298,11 +320,27 @@ export const MutableDaemonConfigSchema = z
298
320
  mcp: z
299
321
  .object({
300
322
  injectIntoAgents: z.boolean(),
323
+ // Daemon-wide Otto tool-group allowlist for the MCP (Claude) path.
324
+ // undefined = all groups enabled (mirrors openai-compat's per-provider
325
+ // ottoToolGroups semantics). An empty array = no Otto tools. Read by the
326
+ // MCP catalog gating (WP-A).
327
+ toolGroups: z.array(z.enum(OTTO_TOOL_GROUPS)).optional(),
301
328
  })
302
329
  .passthrough(),
303
330
  browserTools: MutableBrowserToolsConfigSchema.default({ enabled: false }),
331
+ // Daemon-wide agent behavior toggles (Claude-tier capabilities). Defaults to
332
+ // all-on so a new client parsing an old daemon's config sees today's behavior.
333
+ agentBehaviors: MutableAgentBehaviorsConfigSchema.default({
334
+ promptSuggestions: true,
335
+ agentProgressSummaries: true,
336
+ notifyOnFinishDefault: true,
337
+ }),
304
338
  providers: z.record(z.string(), MutableDaemonProviderConfigSchema).default({}),
305
- metadataGeneration: MutableMetadataGenerationConfigSchema.default({ providers: [] }),
339
+ metadataGeneration: MutableMetadataGenerationConfigSchema.default({
340
+ providers: [],
341
+ enabled: true,
342
+ preferWriterPersonalities: false,
343
+ }),
306
344
  autoArchiveAfterMerge: z.boolean().default(false),
307
345
  enableTerminalAgentHooks: z.boolean().default(false),
308
346
  appendSystemPrompt: z.string().default(""),
@@ -329,6 +367,8 @@ export const MutableDaemonConfigPatchSchema = z
329
367
  .object({
330
368
  mcp: MutableDaemonConfigSchema.shape.mcp.partial().optional(),
331
369
  browserTools: MutableBrowserToolsConfigSchema.partial().optional(),
370
+ // Gated by server_info features.agentBehaviorToggles; patches deep-merge.
371
+ agentBehaviors: MutableAgentBehaviorsConfigSchema.partial().optional(),
332
372
  // A null entry removes the provider's config entirely (custom provider
333
373
  // uninstall). Gated by server_info features.providerRemove — old daemons
334
374
  // reject null values.
@@ -480,6 +520,8 @@ const ContextCompositionSchema = z.object({
480
520
  const AgentUsageSchema = z.object({
481
521
  inputTokens: z.number().optional(),
482
522
  cachedInputTokens: z.number().optional(),
523
+ // Cache-write (prompt-cache creation) tokens; Claude-specific, optional/additive.
524
+ cacheCreationInputTokens: z.number().optional(),
483
525
  outputTokens: z.number().optional(),
484
526
  totalCostUsd: z.number().optional(),
485
527
  contextWindowMaxTokens: z.number().optional(),
@@ -1455,6 +1497,26 @@ export const ActivityCountersSchema = z.object({
1455
1497
  toolsCalled: z.number().default(0),
1456
1498
  artifactsCreated: z.number().default(0),
1457
1499
  schedulesExecuted: z.number().default(0),
1500
+ // Usage & cost accounting (WP-G). Additive/defaulted like every counter above,
1501
+ // so old daemons emit 0 and old clients drop the unknown leaves. "In"/"Out"
1502
+ // are token totals; *CostMicroUsd are integer micro-USD (usd*1e6) to stay
1503
+ // summable — populated only for turns reporting a real provider cost (Claude).
1504
+ // The client detects whether the daemon actually populates these via
1505
+ // features.usageCostCategories (see below).
1506
+ costMicroUsd: z.number().default(0),
1507
+ mainChatTokensIn: z.number().default(0),
1508
+ mainChatTokensOut: z.number().default(0),
1509
+ mainChatCostMicroUsd: z.number().default(0),
1510
+ generationsTokensIn: z.number().default(0),
1511
+ generationsTokensOut: z.number().default(0),
1512
+ generationsCostMicroUsd: z.number().default(0),
1513
+ subagentTokensIn: z.number().default(0),
1514
+ subagentTokensOut: z.number().default(0),
1515
+ subagentCostMicroUsd: z.number().default(0),
1516
+ compactionTokensIn: z.number().default(0),
1517
+ compactionTokensOut: z.number().default(0),
1518
+ claudeTokensIn: z.number().default(0),
1519
+ claudeTokensOut: z.number().default(0),
1458
1520
  });
1459
1521
  export const StatsActivityGetRequestMessageSchema = z.object({
1460
1522
  type: z.literal("stats.activity.get.request"),
@@ -1482,6 +1544,100 @@ export const StatsActivityGetResponseMessageSchema = z.object({
1482
1544
  export const ActivityStatsChangedSchema = z.object({
1483
1545
  type: z.literal("activity_stats_changed"),
1484
1546
  });
1547
+ // One itemized row of the usage ledger — a single token/cost-bearing activity
1548
+ // (a chat turn, a sub-agent turn, or a background generation). The aggregate
1549
+ // ActivityCounters above are the rollup of this same event stream; the ledger is
1550
+ // the scrollable detail behind the tiles (usage-ledger project). `kind` and
1551
+ // `provider` are plain strings (not enums) so an OLD client still parses a NEW
1552
+ // daemon that emits a kind it hasn't heard of — it renders it generically rather
1553
+ // than failing the whole message. All token/cost leaves default to 0.
1554
+ export const UsageEventSchema = z.object({
1555
+ /** Stable unique id for the row (daemon-generated). */
1556
+ id: z.string(),
1557
+ /** Epoch milliseconds when the activity was recorded. */
1558
+ at: z.number(),
1559
+ /** "chat" | "subagent" | "generation" today; open for future kinds. */
1560
+ kind: z.string(),
1561
+ /** Finer label within the kind (e.g. a generation's purpose, a sub-agent name). */
1562
+ subtype: z.string().optional(),
1563
+ /** Agent provider id (e.g. "claude", an openai-compat endpoint id). */
1564
+ provider: z.string(),
1565
+ /** Model id/name if known at the increment site. */
1566
+ model: z.string().optional(),
1567
+ /** input + cached + cache-creation tokens (same "in" split the counters use). */
1568
+ tokensIn: z.number().default(0),
1569
+ /**
1570
+ * The portion of `tokensIn` served from the provider prompt cache (cache-read),
1571
+ * billed at a fraction of fresh input. The fresh (full-rate) portion is
1572
+ * `tokensIn - cachedTokensIn`. Absent when the provider reports no cache reads
1573
+ * (e.g. openai-compat endpoints with no caching), which reads as all-fresh.
1574
+ */
1575
+ cachedTokensIn: z.number().optional(),
1576
+ /** output tokens. */
1577
+ tokensOut: z.number().default(0),
1578
+ /** Real provider spend in integer micro-USD (usd*1e6); 0 for token-only providers. */
1579
+ costMicroUsd: z.number().default(0),
1580
+ /** Mid-turn compaction slice folded into this turn's usage, if any (token-only). */
1581
+ compactionTokensIn: z.number().optional(),
1582
+ compactionTokensOut: z.number().optional(),
1583
+ /** The agent this activity belonged to, for tracing back to the chat. */
1584
+ agentId: z.string().optional(),
1585
+ /**
1586
+ * How many model round-trips this row aggregates. A chat row is one query, but
1587
+ * a sub-agent row covers a whole delegated task that internally ran many
1588
+ * rounds — and each round re-reads the growing context, so `cachedTokensIn` is
1589
+ * cumulative cache-READS, not a cache size. Surfacing the count is what makes a
1590
+ * large cached figure legible instead of looking like a bug. Absent when the
1591
+ * provider doesn't report it.
1592
+ */
1593
+ rounds: z.number().optional(),
1594
+ /**
1595
+ * Sub-agent rows only — the spawn-tree identity that lets the Log group rows
1596
+ * the way a human reads the run (chat turn → its sub-agents → their
1597
+ * sub-agents) instead of by settle time, which async sub-agents crossing turn
1598
+ * boundaries makes wrong. `startedAt` is when the sub-agent was first
1599
+ * observed (epoch ms; a row belongs to the turn that spawned it, not the turn
1600
+ * it happened to settle in), `subagentKey` is its stable observed key, and
1601
+ * `parentSubagentKey` is the spawning sub-agent's key — absent for depth-1
1602
+ * sub-agents spawned by the chat itself.
1603
+ */
1604
+ startedAt: z.number().optional(),
1605
+ subagentKey: z.string().optional(),
1606
+ parentSubagentKey: z.string().optional(),
1607
+ });
1608
+ export const UsageLogGetRequestMessageSchema = z.object({
1609
+ type: z.literal("usage.log.get.request"),
1610
+ requestId: z.string(),
1611
+ /** Max rows to return (daemon clamps). Newest-first. */
1612
+ limit: z.number().optional(),
1613
+ /** Cursor: return only rows strictly older than this epoch-ms (for "load more"). */
1614
+ before: z.number().optional(),
1615
+ });
1616
+ export const UsageLogGetResponseMessageSchema = z.object({
1617
+ type: z.literal("usage.log.get.response"),
1618
+ payload: z.object({
1619
+ requestId: z.string(),
1620
+ /** Newest-first page of ledger rows. */
1621
+ events: z.array(UsageEventSchema).default([]),
1622
+ /** True when older rows exist beyond this page (paginate with `before`). */
1623
+ hasMore: z.boolean().default(false),
1624
+ }),
1625
+ });
1626
+ // Wipe every daemon-wide usage counter AND the itemized usage ledger back to
1627
+ // zero — the "Reset" action on the Metrics screen. One RPC clears both sinks
1628
+ // (the day-bucketed ActivityStatsStore and the UsageLogStore) so the tiles and
1629
+ // the Log tab start fresh together. Gated behind features.statsReset so an old
1630
+ // daemon (no handler) never receives a request the client thinks it can send.
1631
+ export const StatsActivityResetRequestMessageSchema = z.object({
1632
+ type: z.literal("stats.activity.reset.request"),
1633
+ requestId: z.string(),
1634
+ });
1635
+ export const StatsActivityResetResponseMessageSchema = z.object({
1636
+ type: z.literal("stats.activity.reset.response"),
1637
+ payload: z.object({
1638
+ requestId: z.string(),
1639
+ }),
1640
+ });
1485
1641
  export const AgentContextGetUsageRequestMessageSchema = z.object({
1486
1642
  type: z.literal("agent.context.get_usage.request"),
1487
1643
  agentId: z.string(),
@@ -1694,6 +1850,164 @@ export const SuggestedTasksChangedSchema = z.object({
1694
1850
  tasks: z.array(SuggestedTaskInfoSchema),
1695
1851
  }),
1696
1852
  });
1853
+ // Context Management — the daemon's accounting of everything a provider sends
1854
+ // before the user types (see projects/context-management/context-management.md).
1855
+ //
1856
+ // Two distinctions carry the whole feature and must not be collapsed on the
1857
+ // wire: an `import` edge is inlined into the request while a `reference` edge
1858
+ // costs only its link text, and `costClass` separates weight that rides every
1859
+ // request from weight that loads only when the agent touches an area.
1860
+ //
1861
+ // All numbers are estimates (chars/4) and `confidence` says how much to trust
1862
+ // the file set: `exact` when Otto composed the payload itself, `convention`
1863
+ // when resolved from a provider's documented layout, `unverified` for
1864
+ // subprocess-owned agents we cannot see into.
1865
+ // COMPAT(contextManagement): added in v0.6.5, drop the gate when daemon floor >= v0.6.5.
1866
+ export const ContextScopeSchema = z.enum([
1867
+ "enterprise",
1868
+ "global",
1869
+ "project",
1870
+ "local",
1871
+ "subdirectory",
1872
+ "runtime",
1873
+ ]);
1874
+ export const ContextCategorySchema = z.enum([
1875
+ "context_files",
1876
+ "memory_index",
1877
+ "skills_roster",
1878
+ "mcp_tools",
1879
+ "otto_injected",
1880
+ "system_prompt",
1881
+ ]);
1882
+ export const ContextCostClassSchema = z.enum(["fixed", "conditional", "referenced"]);
1883
+ export const ContextSeveritySchema = z.enum(["ok", "notice", "warn", "critical"]);
1884
+ export const ContextConfidenceSchema = z.enum(["exact", "convention", "unverified"]);
1885
+ export const ContextFindingKindSchema = z.enum([
1886
+ "dead_import",
1887
+ "dead_reference",
1888
+ "duplicate_across_scope",
1889
+ "duplicate_within_file",
1890
+ "oversized_memory_entry",
1891
+ "import_cycle",
1892
+ "depth_capped",
1893
+ ]);
1894
+ export const ContextRangeSchema = z.object({
1895
+ start: z.number(),
1896
+ end: z.number(),
1897
+ });
1898
+ export const ContextFindingSchema = z.object({
1899
+ kind: ContextFindingKindSchema,
1900
+ message: z.string(),
1901
+ range: ContextRangeSchema.optional(),
1902
+ relatedNodeIds: z.array(z.string()).optional(),
1903
+ // The node this finding is about. Redundant while the finding sits on its
1904
+ // node, load-bearing once the report flattens them all into one list — that
1905
+ // list is the "Worth fixing" tab, and without this a row cannot say where it
1906
+ // came from or take you there.
1907
+ nodeId: z.string().optional(),
1908
+ // 1-based line of `range.start` in that node's file, so the fix list can jump
1909
+ // the editor without the client re-reading bytes to count newlines.
1910
+ line: z.number().optional(),
1911
+ // Last line of the range, so the client can select the whole offending span
1912
+ // rather than dropping a cursor at the top of it.
1913
+ lineEnd: z.number().optional(),
1914
+ });
1915
+ export const ContextNodeSchema = z.object({
1916
+ id: z.string(),
1917
+ path: z.string(),
1918
+ relPath: z.string(),
1919
+ scope: ContextScopeSchema,
1920
+ category: ContextCategorySchema,
1921
+ costClass: ContextCostClassSchema,
1922
+ bytes: z.number(),
1923
+ estTokens: z.number(),
1924
+ // Extra parents that also reach this node. The node is listed and counted
1925
+ // exactly once; these render as a dimmed "also imported by" chip.
1926
+ alsoImportedByNodeIds: z.array(z.string()),
1927
+ findings: z.array(ContextFindingSchema),
1928
+ });
1929
+ export const ContextEdgeSchema = z.object({
1930
+ fromNodeId: z.string(),
1931
+ // Null when the target could not be resolved — pairs with a dead_* finding.
1932
+ toNodeId: z.string().nullable(),
1933
+ kind: z.enum(["import", "reference"]),
1934
+ rawTarget: z.string(),
1935
+ // Byte range of the whole reference token in the parent file, which is what
1936
+ // makes "Always load" <-> "Link only" a single-span edit.
1937
+ range: ContextRangeSchema,
1938
+ });
1939
+ export const ContextCategoryTotalSchema = z.object({
1940
+ category: ContextCategorySchema,
1941
+ estTokens: z.number(),
1942
+ sharePercent: z.number(),
1943
+ severity: ContextSeveritySchema,
1944
+ });
1945
+ export const ContextReportSchema = z.object({
1946
+ workspaceId: z.string(),
1947
+ provider: z.string(),
1948
+ // The window the report was evaluated against — from the active model, or
1949
+ // the client's what-if picker. Severity is meaningless without it.
1950
+ windowTokens: z.number(),
1951
+ scannedAt: z.string(),
1952
+ confidence: ContextConfidenceSchema,
1953
+ supported: z.boolean(),
1954
+ supportsImports: z.boolean(),
1955
+ nodes: z.array(ContextNodeSchema),
1956
+ edges: z.array(ContextEdgeSchema),
1957
+ categoryTotals: z.array(ContextCategoryTotalSchema),
1958
+ fixedTotal: z.number(),
1959
+ conditionalTotal: z.number(),
1960
+ referencedTotal: z.number(),
1961
+ workingRoom: z.number(),
1962
+ aggregateSeverity: ContextSeveritySchema,
1963
+ findings: z.array(ContextFindingSchema),
1964
+ });
1965
+ // Pushed with the full current report whenever a watched context file changes.
1966
+ // Full-report reconciliation, same idiom as suggested_tasks_changed.
1967
+ export const ContextReportChangedSchema = z.object({
1968
+ type: z.literal("context_report_changed"),
1969
+ payload: z.object({
1970
+ workspaceId: z.string(),
1971
+ report: ContextReportSchema.nullable(),
1972
+ }),
1973
+ });
1974
+ // `provider` and `windowTokens` are the what-if pickers: omitted means "the
1975
+ // active agent's provider and its model's real window".
1976
+ export const ContextReportGetRequestMessageSchema = z.object({
1977
+ type: z.literal("context.report.get.request"),
1978
+ requestId: z.string(),
1979
+ workspaceId: z.string(),
1980
+ provider: z.string().optional(),
1981
+ windowTokens: z.number().optional(),
1982
+ });
1983
+ export const ContextReportGetResponseMessageSchema = z.object({
1984
+ type: z.literal("context.report.get.response"),
1985
+ payload: z.object({
1986
+ requestId: z.string(),
1987
+ report: ContextReportSchema.nullable(),
1988
+ }),
1989
+ });
1990
+ // Converts one edge between "always loaded" and "link only". Server-side
1991
+ // because the parent file may live outside the workspace root.
1992
+ export const ContextEdgeConvertRequestMessageSchema = z.object({
1993
+ type: z.literal("context.edge.convert.request"),
1994
+ requestId: z.string(),
1995
+ workspaceId: z.string(),
1996
+ // The parent file holding the reference — its `ContextNode.path`, not its
1997
+ // id: ids are case-folded on Windows and are not safe to write through.
1998
+ filePath: z.string(),
1999
+ rawTarget: z.string(),
2000
+ range: ContextRangeSchema,
2001
+ target: z.enum(["import", "reference"]),
2002
+ });
2003
+ export const ContextEdgeConvertResponseMessageSchema = z.object({
2004
+ type: z.literal("context.edge.convert.response"),
2005
+ payload: z.object({
2006
+ requestId: z.string(),
2007
+ ok: z.boolean(),
2008
+ error: z.string().optional(),
2009
+ }),
2010
+ });
1697
2011
  // Aggregate outcome for a start/dismiss over one or more tasks. `succeeded`/
1698
2012
  // `failed` count the tasks acted on so the client can report "Started 3 tasks";
1699
2013
  // `error` collects any per-task failure messages (the failed tasks' chips stay).
@@ -2723,6 +3037,10 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
2723
3037
  ProviderDiagnosticRequestMessageSchema,
2724
3038
  ProviderUsageListRequestMessageSchema,
2725
3039
  StatsActivityGetRequestMessageSchema,
3040
+ ContextReportGetRequestMessageSchema,
3041
+ ContextEdgeConvertRequestMessageSchema,
3042
+ StatsActivityResetRequestMessageSchema,
3043
+ UsageLogGetRequestMessageSchema,
2726
3044
  AgentContextGetUsageRequestMessageSchema,
2727
3045
  ResumeAgentRequestMessageSchema,
2728
3046
  ImportAgentRequestMessageSchema,
@@ -3036,8 +3354,19 @@ export const ServerInfoStatusPayloadSchema = z
3036
3354
  observedSubagents: z.boolean().optional(),
3037
3355
  // COMPAT(backgroundShellTasks): added in v0.5.3, drop the gate when daemon floor >= v0.5.3.
3038
3356
  backgroundShellTasks: z.boolean().optional(),
3357
+ // COMPAT(retainedTranscripts): added in v0.6.4, drop the gate when daemon floor >= v0.6.4.
3358
+ // Daemon retains schedule/artifact generation-agent chats for read-only
3359
+ // viewing after the run. See docs/safe-unattended.md.
3360
+ retainedTranscripts: z.boolean().optional(),
3039
3361
  // COMPAT(suggestedTasks): added in v0.5.6, drop the gate when daemon floor >= v0.5.6.
3040
3362
  suggestedTasks: z.boolean().optional(),
3363
+ // Daemon can resolve and evaluate the provider's context graph, serve
3364
+ // context.report.* and push context_report_changed. Without it the
3365
+ // client hides both the Context Management tab and the composer
3366
+ // warning entirely — there is no degraded client-side fallback, since
3367
+ // only the daemon can see the files a provider loads.
3368
+ // COMPAT(contextManagement): added in v0.6.5, drop the gate when daemon floor >= v0.6.5.
3369
+ contextManagement: z.boolean().optional(),
3041
3370
  // COMPAT(textEditor): added in v0.4.4, drop the gate when daemon floor >= v0.4.4.
3042
3371
  textEditor: z.boolean().optional(),
3043
3372
  // COMPAT(projectSearch): added in v0.4.4, drop the gate when daemon floor >= v0.4.4.
@@ -3093,6 +3422,44 @@ export const ServerInfoStatusPayloadSchema = z
3093
3422
  // Set when the daemon emits agent_stream `rate_limit_updated` events (Claude
3094
3423
  // plan rate-limit status). Warnings degrade silently on an old daemon (no event).
3095
3424
  rateLimitEvents: z.boolean().optional(),
3425
+ // COMPAT(openaiCompatMaxToolRounds): added in v0.6.4, drop the gate when daemon floor >= v0.6.4.
3426
+ // Set when the daemon honors the provider-level `maxToolRounds` override for
3427
+ // openai-compat agents. The client gates the Agents-tab control on this so an
3428
+ // old daemon (which silently ignores the field and keeps the fixed 50-round
3429
+ // cap) shows "Update the host" instead of a knob that does nothing.
3430
+ openaiCompatMaxToolRounds: z.boolean().optional(),
3431
+ // COMPAT(mcpToolGroups): added in v0.6.4, drop the gate when daemon floor >= v0.6.4.
3432
+ // Set when the daemon honors `mcp.toolGroups` — per-group gating of the
3433
+ // Otto tool catalog on the MCP (Claude) path. Old daemons register every
3434
+ // group regardless, so the client hides the categorized section instead
3435
+ // of showing category switches that do nothing.
3436
+ mcpToolGroups: z.boolean().optional(),
3437
+ // COMPAT(agentBehaviorToggles): added in v0.6.4, drop the gate when daemon floor >= v0.6.4.
3438
+ // Set when the daemon persists `agentBehaviors.*` (promptSuggestions,
3439
+ // agentProgressSummaries, notifyOnFinishDefault). The reads are wired by
3440
+ // Claude-tier providers (WP-E); the client gates the toggle cards on this.
3441
+ agentBehaviorToggles: z.boolean().optional(),
3442
+ // COMPAT(metadataGenerationEnabled): added in v0.6.4, drop the gate when daemon floor >= v0.6.4.
3443
+ // Set when the daemon persists `metadataGeneration.{enabled,preferWriterPersonalities}`.
3444
+ // The generation path (WP-B) reads them; the client gates the toggle cards on this.
3445
+ metadataGenerationEnabled: z.boolean().optional(),
3446
+ // COMPAT(usageCostCategories): added in v0.6.4, drop the gate when daemon floor >= v0.6.4.
3447
+ // Set when the daemon populates the per-category token/cost counters in
3448
+ // ActivityCounters (mainChat/generations/subagents/compaction + Claude
3449
+ // provider split + micro-USD cost). An old daemon leaves them all at 0,
3450
+ // so the client hides the Usage & Cost column's category grid rather than
3451
+ // presenting a column of zeros as if it were truthful accounting.
3452
+ usageCostCategories: z.boolean().optional(),
3453
+ // COMPAT(usageLog): added in v0.6.4, drop the gate when daemon floor >= v0.6.4.
3454
+ // Set when the daemon serves the itemized usage ledger (usage.log.get).
3455
+ // The client gates the Metrics screen's "Log" tab on this; an old daemon
3456
+ // simply doesn't offer the tab.
3457
+ usageLog: z.boolean().optional(),
3458
+ // COMPAT(statsReset): added in v0.6.4, drop the gate when daemon floor >= v0.6.4.
3459
+ // Set when the daemon handles stats.activity.reset (wipe all usage
3460
+ // counters + the itemized ledger). The client gates the Metrics screen's
3461
+ // "Reset" button on this; an old daemon simply doesn't offer it.
3462
+ statsReset: z.boolean().optional(),
3096
3463
  })
3097
3464
  .optional(),
3098
3465
  })
@@ -5278,6 +5645,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
5278
5645
  TasksSuggestedStartResponseMessageSchema,
5279
5646
  TasksSuggestedDismissResponseMessageSchema,
5280
5647
  SuggestedTasksChangedSchema,
5648
+ ContextReportChangedSchema,
5281
5649
  AgentPersonalitySetResponseMessageSchema,
5282
5650
  AgentRewindResponseMessageSchema,
5283
5651
  UpdateAgentResponseMessageSchema,
@@ -5363,6 +5731,10 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
5363
5731
  ProviderDiagnosticResponseMessageSchema,
5364
5732
  ProviderUsageListResponseMessageSchema,
5365
5733
  StatsActivityGetResponseMessageSchema,
5734
+ ContextReportGetResponseMessageSchema,
5735
+ ContextEdgeConvertResponseMessageSchema,
5736
+ StatsActivityResetResponseMessageSchema,
5737
+ UsageLogGetResponseMessageSchema,
5366
5738
  ActivityStatsChangedSchema,
5367
5739
  AgentContextGetUsageResponseMessageSchema,
5368
5740
  ListCommandsResponseSchema,
@@ -86,6 +86,16 @@ export declare const MCP_TOOL_PERMISSION_MODES: readonly ["always-ask", "trust-r
86
86
  * conversation is compacted automatically.
87
87
  */
88
88
  export declare const COMPACTION_THRESHOLD_PERCENTS: readonly [50, 60, 70, 80, 90];
89
+ /**
90
+ * Max model→tool→model rounds per turn for providers whose tool loop the
91
+ * daemon owns (openai-compat). The turn stops with an error after this many
92
+ * rounds without a final answer — a runaway-loop safety valve, most often hit
93
+ * by smaller local models that keep calling tools instead of converging.
94
+ * Bounds keep the setting a sane guard rail rather than an off switch.
95
+ */
96
+ export declare const MAX_TOOL_ROUNDS_DEFAULT = 50;
97
+ export declare const MAX_TOOL_ROUNDS_MIN = 1;
98
+ export declare const MAX_TOOL_ROUNDS_MAX = 1000;
89
99
  /**
90
100
  * Compaction tuning for providers whose conversation the daemon owns
91
101
  * (openai-compat). These set the provider-level defaults; the per-agent
@@ -167,6 +177,7 @@ export declare const ProviderOverrideSchema: z.ZodObject<{
167
177
  keepRecentTokens: z.ZodOptional<z.ZodNumber>;
168
178
  hideSelector: z.ZodOptional<z.ZodBoolean>;
169
179
  }, z.core.$strip>>;
180
+ maxToolRounds: z.ZodOptional<z.ZodNumber>;
170
181
  enabled: z.ZodOptional<z.ZodBoolean>;
171
182
  order: z.ZodOptional<z.ZodNumber>;
172
183
  }, z.core.$strip>;
@@ -239,6 +250,7 @@ export declare const ProviderOverridesSchema: z.ZodRecord<z.ZodString, z.ZodObje
239
250
  keepRecentTokens: z.ZodOptional<z.ZodNumber>;
240
251
  hideSelector: z.ZodOptional<z.ZodBoolean>;
241
252
  }, z.core.$strip>>;
253
+ maxToolRounds: z.ZodOptional<z.ZodNumber>;
242
254
  enabled: z.ZodOptional<z.ZodBoolean>;
243
255
  order: z.ZodOptional<z.ZodNumber>;
244
256
  }, z.core.$strip>>;
@@ -117,6 +117,16 @@ export const MCP_TOOL_PERMISSION_MODES = ["always-ask", "trust-read-only"];
117
117
  * conversation is compacted automatically.
118
118
  */
119
119
  export const COMPACTION_THRESHOLD_PERCENTS = [50, 60, 70, 80, 90];
120
+ /**
121
+ * Max model→tool→model rounds per turn for providers whose tool loop the
122
+ * daemon owns (openai-compat). The turn stops with an error after this many
123
+ * rounds without a final answer — a runaway-loop safety valve, most often hit
124
+ * by smaller local models that keep calling tools instead of converging.
125
+ * Bounds keep the setting a sane guard rail rather than an off switch.
126
+ */
127
+ export const MAX_TOOL_ROUNDS_DEFAULT = 50;
128
+ export const MAX_TOOL_ROUNDS_MIN = 1;
129
+ export const MAX_TOOL_ROUNDS_MAX = 1000;
120
130
  /**
121
131
  * Compaction tuning for providers whose conversation the daemon owns
122
132
  * (openai-compat). These set the provider-level defaults; the per-agent
@@ -165,6 +175,11 @@ export const ProviderOverrideSchema = z.object({
165
175
  * (openai-compat). Per-agent feature values win over these.
166
176
  */
167
177
  compaction: ProviderCompactionConfigSchema.optional(),
178
+ /**
179
+ * Max model→tool→model rounds per turn for daemon-hosted providers
180
+ * (openai-compat). Omitted = the built-in default (MAX_TOOL_ROUNDS_DEFAULT).
181
+ */
182
+ maxToolRounds: z.number().int().min(MAX_TOOL_ROUNDS_MIN).max(MAX_TOOL_ROUNDS_MAX).optional(),
168
183
  enabled: z.boolean().optional(),
169
184
  order: z.number().optional(),
170
185
  });
@@ -124,6 +124,19 @@ const MOCK_LOAD_TEST_MODES = [
124
124
  icon: "ShieldOff",
125
125
  colorTier: "dangerous",
126
126
  },
127
+ {
128
+ id: "dontAsk",
129
+ label: "Don't Ask",
130
+ description: "Runs without prompting — actions not pre-approved are denied",
131
+ icon: "ShieldCheck",
132
+ colorTier: "moderate",
133
+ // Dev-only mirror of Claude's guarded unattended mode so deterministic E2E
134
+ // specs can exercise the system-assigned surfaces: the unattended coercion
135
+ // target for mock schedule runs, and the locked mode badge on a live agent
136
+ // stuck in a non-user-selectable mode.
137
+ isUnattended: true,
138
+ userSelectable: false,
139
+ },
127
140
  ];
128
141
  const MOCK_SLOW_MODES = [
129
142
  {