@qodeca/xezar 0.11.2 → 0.13.0

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.
Files changed (138) hide show
  1. package/README.md +69 -18
  2. package/dist/agent-config/catalog.d.ts +10 -0
  3. package/dist/agent-config/catalog.js.map +1 -1
  4. package/dist/automations/coordinator.js +8 -0
  5. package/dist/automations/coordinator.js.map +1 -1
  6. package/dist/automations/types.d.ts +2 -0
  7. package/dist/contract/automations.d.ts +8 -0
  8. package/dist/contract/events.test.d.ts +1 -0
  9. package/dist/contract/index.js +87 -3
  10. package/dist/contract/runs.d.ts +25 -0
  11. package/dist/contract/runs.test.d.ts +1 -0
  12. package/dist/contract/skills.d.ts +1 -0
  13. package/dist/contract/workflows.d.ts +6 -0
  14. package/dist/contract/workspace.d.ts +37 -4
  15. package/dist/core/agent-env.js +8 -1
  16. package/dist/core/agent-env.js.map +1 -1
  17. package/dist/core/agent-runner.d.ts +13 -0
  18. package/dist/core/agent-runner.js +22 -0
  19. package/dist/core/agent-runner.js.map +1 -1
  20. package/dist/core/backend-detect.js +11 -1
  21. package/dist/core/backend-detect.js.map +1 -1
  22. package/dist/core/claude-cli-runner.js +8 -2
  23. package/dist/core/claude-cli-runner.js.map +1 -1
  24. package/dist/core/codex-app-server-runner.js +6 -2
  25. package/dist/core/codex-app-server-runner.js.map +1 -1
  26. package/dist/core/opencode-server-runner.d.ts +12 -0
  27. package/dist/core/opencode-server-runner.js +326 -17
  28. package/dist/core/opencode-server-runner.js.map +1 -1
  29. package/dist/core/pi-model-catalog.d.ts +52 -0
  30. package/dist/core/pi-model-catalog.js +135 -0
  31. package/dist/core/pi-model-catalog.js.map +1 -0
  32. package/dist/core/pi-runner.d.ts +3 -0
  33. package/dist/core/pi-runner.js +139 -12
  34. package/dist/core/pi-runner.js.map +1 -1
  35. package/dist/core/pi-ui-mapper.js +39 -3
  36. package/dist/core/pi-ui-mapper.js.map +1 -1
  37. package/dist/core/runner-model-catalog.js +11 -2
  38. package/dist/core/runner-model-catalog.js.map +1 -1
  39. package/dist/index.js +2 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/paths.d.ts +7 -0
  42. package/dist/paths.js +8 -0
  43. package/dist/paths.js.map +1 -1
  44. package/dist/runs/event-corrections.d.ts +12 -0
  45. package/dist/runs/event-corrections.js +141 -0
  46. package/dist/runs/event-corrections.js.map +1 -0
  47. package/dist/runs/event-history.d.ts +5 -1
  48. package/dist/runs/event-history.js +66 -5
  49. package/dist/runs/event-history.js.map +1 -1
  50. package/dist/runs/project-writer.d.ts +11 -0
  51. package/dist/runs/project-writer.js +115 -0
  52. package/dist/runs/project-writer.js.map +1 -0
  53. package/dist/runs/store.d.ts +2 -0
  54. package/dist/runs/store.js +13 -2
  55. package/dist/runs/store.js.map +1 -1
  56. package/dist/runs/task-markers.d.ts +4 -0
  57. package/dist/runs/task-markers.js +63 -5
  58. package/dist/runs/task-markers.js.map +1 -1
  59. package/dist/server/capabilities.d.ts +7 -5
  60. package/dist/server/capabilities.js +11 -7
  61. package/dist/server/capabilities.js.map +1 -1
  62. package/dist/server/project-context.js +2 -0
  63. package/dist/server/project-context.js.map +1 -1
  64. package/dist/server/server.d.ts +199 -47
  65. package/dist/server/server.js +195 -11
  66. package/dist/server/server.js.map +1 -1
  67. package/dist/workflows/run.d.ts +67 -2
  68. package/dist/workflows/run.js +190 -44
  69. package/dist/workflows/run.js.map +1 -1
  70. package/dist/workflows/types.d.ts +41 -0
  71. package/dist/workflows/types.js +72 -0
  72. package/dist/workflows/types.js.map +1 -1
  73. package/dist/workspace/config.d.ts +42 -0
  74. package/dist/workspace/config.js +132 -3
  75. package/dist/workspace/config.js.map +1 -1
  76. package/dist/workspace/semaphore.d.ts +63 -3
  77. package/dist/workspace/semaphore.js +76 -3
  78. package/dist/workspace/semaphore.js.map +1 -1
  79. package/package.json +3 -3
  80. package/web/dist/assets/{alert-dialog-CGskMvgc.js → alert-dialog-BVsVcbFy.js} +1 -1
  81. package/web/dist/assets/arrow-down-CYgAlZtW.js +1 -0
  82. package/web/dist/assets/arrow-left-BOqE0a3k.js +1 -0
  83. package/web/dist/assets/{centered-state-B_V_Ai2h.js → centered-state-Djgly6Ni.js} +1 -1
  84. package/web/dist/assets/chevron-right-Cc9BWnbN.js +1 -0
  85. package/web/dist/assets/{collapsible-B_8uYwHb.js → collapsible-DZ4D-DHG.js} +1 -1
  86. package/web/dist/assets/{commit-list-C4Dv6vUL.js → commit-list-B-z1JL9G.js} +1 -1
  87. package/web/dist/assets/{compare-variants-et0mplGi.js → compare-variants-CdS8r8E2.js} +1 -1
  88. package/web/dist/assets/{diff-CbozNUJq.js → diff-BKew9rQX.js} +2 -2
  89. package/web/dist/assets/{diff-stat-btAmKFJr.js → diff-stat-D0mc5V6Z.js} +1 -1
  90. package/web/dist/assets/{diff-view-BpFTcG3R.js → diff-view-xpIcuLIC.js} +1 -1
  91. package/web/dist/assets/{dropdown-menu-Z80KMInL.js → dropdown-menu-CEQziH7X.js} +1 -1
  92. package/web/dist/assets/editable-title-BCX3T2BY.js +1 -0
  93. package/web/dist/assets/{ellipsis-vertical-Bmi2u2TG.js → ellipsis-vertical-ByVdDc4z.js} +1 -1
  94. package/web/dist/assets/{file-DKoYWRsv.js → file-xBSB5hXO.js} +1 -1
  95. package/web/dist/assets/{folder-u01HScRT.js → folder-CldgbZ5A.js} +1 -1
  96. package/web/dist/assets/{git-toolbar-Q2kk2F7v.js → git-toolbar-BYlnf4qf.js} +1 -1
  97. package/web/dist/assets/github-CqGK62Lw.js +1 -0
  98. package/web/dist/assets/{image-preview-BR3iIIhf.js → image-preview-Cv5mZr0n.js} +1 -1
  99. package/web/dist/assets/index-3nxVmdn5.css +2 -0
  100. package/web/dist/assets/index-qX7n11FX.js +9 -0
  101. package/web/dist/assets/{markdown-5FuCZDA4.js → markdown-CgEEE2ND.js} +1 -1
  102. package/web/dist/assets/{new-task-form-BydHBbef.js → new-task-form-1r2bgsdD.js} +1 -1
  103. package/web/dist/assets/{pill-vNzaaYxj.js → pill-q-x22_ej.js} +1 -1
  104. package/web/dist/assets/{project-router-BbCzrHKs.js → project-router-VDN4jQM8.js} +1 -1
  105. package/web/dist/assets/{prompt-templates-B7Fs2b1S.js → prompt-templates-BmZtCe-L.js} +1 -1
  106. package/web/dist/assets/{refresh-cw-DGliez3E.js → refresh-cw-DcNCj2nu.js} +1 -1
  107. package/web/dist/assets/{repo-git-h1RFfly3.js → repo-git-C2On8SlI.js} +1 -1
  108. package/web/dist/assets/{run-diff-Bq9vkSNK.js → run-diff-vguEMJzh.js} +1 -1
  109. package/web/dist/assets/{run-header-Cd8mSKBG.js → run-header-DM7MiKj9.js} +1 -1
  110. package/web/dist/assets/{search-x-69n_672Y.js → search-x-QmhaAbg5.js} +1 -1
  111. package/web/dist/assets/{skill-empty-hint-CL0g7WBL.js → skill-empty-hint-BrzUwByZ.js} +1 -1
  112. package/web/dist/assets/{skills-BqAzyGS3.js → skills-Bj-uG_Hz.js} +1 -1
  113. package/web/dist/assets/{sparkles-B8agSxBZ.js → sparkles-CP8QDOgX.js} +1 -1
  114. package/web/dist/assets/{square-terminal-Cf4Og6ct.js → square-terminal-CGypshSD.js} +1 -1
  115. package/web/dist/assets/{tab-link-Cp2-CJS8.js → tab-link-D4lFzzI1.js} +1 -1
  116. package/web/dist/assets/{task-changes-DwXPFXqG.js → task-changes-6aopqAmV.js} +1 -1
  117. package/web/dist/assets/{task-commits-Bessua-c.js → task-commits-u-dwaCyw.js} +1 -1
  118. package/web/dist/assets/{task-files-DxFIhRPP.js → task-files-IMOZnIUK.js} +1 -1
  119. package/web/dist/assets/task-thread-CbMPmrWq.js +9 -0
  120. package/web/dist/assets/{textarea--cpYshee.js → textarea-CyBCyKIk.js} +1 -1
  121. package/web/dist/assets/{thread-loading-ZhJref0J.js → thread-loading-Cuq2jchC.js} +1 -1
  122. package/web/dist/assets/{trash-2--ZtrzUx3.js → trash-2-B6Dp3ZNs.js} +1 -1
  123. package/web/dist/assets/{triangle-alert-D5tJwRD2.js → triangle-alert-xBya8FUp.js} +1 -1
  124. package/web/dist/assets/{upload-SnwVxKOA.js → upload-D6MXEB9p.js} +1 -1
  125. package/web/dist/assets/{use-desktop-Sr1QlxhT.js → use-desktop-fLDKEJoB.js} +1 -1
  126. package/web/dist/assets/{use-submit-shortcut-JY8M_MPL.js → use-submit-shortcut-JTpNzU-u.js} +1 -1
  127. package/web/dist/assets/{utils-Bb8ohcr3.js → utils--z9hRtl3.js} +1 -1
  128. package/web/dist/assets/{workflows-9HC240Wx.js → workflows-BY3KTkFw.js} +3 -3
  129. package/web/dist/assets/{zoomable-image-0G3IBXWC.js → zoomable-image-GTYwLtkS.js} +1 -1
  130. package/web/dist/index.html +23 -23
  131. package/web/dist/assets/arrow-down-G8adPhbu.js +0 -1
  132. package/web/dist/assets/arrow-left-BJsIW6A0.js +0 -1
  133. package/web/dist/assets/chevron-right-CFzn-von.js +0 -1
  134. package/web/dist/assets/editable-title-wS5NWwob.js +0 -1
  135. package/web/dist/assets/github-DjEYCxwt.js +0 -1
  136. package/web/dist/assets/index-BcI5hug3.css +0 -2
  137. package/web/dist/assets/index-Cz4GSAqV.js +0 -7
  138. package/web/dist/assets/task-thread-Dhkupf1-.js +0 -9
@@ -1,3 +1,5 @@
1
+ import { EventCorrectionError } from '../runs/event-corrections.js';
2
+ import { validateLegacyHistoryResume } from '../runs/event-history.js';
1
3
  import { projectDataDir } from '../project-data-paths.js';
2
4
  import { projectKitDir } from '../project-kit-paths.js';
3
5
  import { existsSync, readFileSync, statSync } from 'node:fs';
@@ -21,13 +23,14 @@ import { z } from 'zod';
21
23
  import { setWorkspaceUiStateInputSchema, } from '../contract/index.js';
22
24
  // A contract VALUE, like `workspaceUiStateSchema` in workspace/migrations.ts — the request
23
25
  // schema this route validates with is the same one the client compiles against.
24
- import { attachmentInputSchema, modelDiscoveryRunnerSchema, openProjectInSchema, updateProjectInputSchema, } from '../contract/index.js';
26
+ import { attachmentInputSchema, MODEL_DISCOVERY_RUNNERS, modelDiscoveryRunnerSchema, openProjectInSchema, updateProjectInputSchema, } from '../contract/index.js';
25
27
  import { detectEnvironment } from '../core/backend-detect.js';
26
28
  import { RUNNER_IDS } from '../core/agent-runner.js';
27
29
  import { AGENT_MODELS_LOCKED_ERROR, agentModelsLocked } from '../core/agent-model-policy.js';
28
30
  import { discoverClaudeModels } from '../core/claude-model-catalog.js';
29
31
  import { discoverCodexModels } from '../core/codex-model-catalog.js';
30
32
  import { discoverOpencodeModels } from '../core/opencode-model-catalog.js';
33
+ import { discoverPiModels } from '../core/pi-model-catalog.js';
31
34
  import { PROVIDER_IDS, ProviderAuthService, providerAuthChecksDisabled, } from '../core/provider-auth.js';
32
35
  import { applyProviderEnablement } from '../core/provider-availability.js';
33
36
  import { RunnerModelCatalog } from '../core/runner-model-catalog.js';
@@ -56,7 +59,7 @@ import { readAgentModelDefaults } from '../agent-config/models.js';
56
59
  import { listAgentConfig } from '../agent-config/service.js';
57
60
  import { listConfigFiles } from '../agent-config/catalog.js';
58
61
  import { readAccountIdentity } from '../agent-config/account-identity.js';
59
- import { PROJECT_ID_RE, defaultWorkspaceConfig, effectiveSkillsAutoUpdate, loadWorkspaceConfig, mergeWriteWorkspaceConfig, effectiveComposerDefault, } from '../workspace/config.js';
62
+ import { DEFAULT_MEMORY_LIMIT_MB, PROJECT_ID_RE, defaultWorkspaceConfig, effectiveSkillsAutoUpdate, loadWorkspaceConfig, mergeWriteWorkspaceConfig, effectiveComposerDefault, } from '../workspace/config.js';
60
63
  import { CONTROL_CHARS_RE, DEFAULT_AGENT_ACCOUNT_ID, defaultAgentAccountStore, isAbsoluteConfigDir, loadAgentAccounts, mergeWriteAgentAccounts, } from '../workspace/agent-accounts.js';
61
64
  import { defaultAgentProfile, listAgentProfiles, profileDirState, resolveProfileEnvForRoot, resolveStoredProfile, sameProfileDir, } from '../workspace/agent-profiles.js';
62
65
  import { PROFILE_CAPABLE_PROVIDERS, profileEnv, supportsProfiles } from '../core/agent-profiles.js';
@@ -65,6 +68,7 @@ import { allocateProjectSlug, listProjects, normalizeProjectTags, probeProjectSt
65
68
  import { mergeWriteWorkspaceUiState, readWorkspaceUiState } from '../workspace/ui-state.js';
66
69
  import { checkoutRepo } from './checkout.js';
67
70
  import { ProjectContextError, ProjectContexts } from './project-context.js';
71
+ import { ProjectWriterError } from '../runs/project-writer.js';
68
72
  import { reviewGateEnabled } from '../runs/review-gate.js';
69
73
  import { readUiState, uiStatePath } from '../ui-state.js';
70
74
  import { agentHomePaths, expandTilde } from '../paths.js';
@@ -685,6 +689,9 @@ export function createApp(deps) {
685
689
  claude: { discover: () => discoverClaudeModels({ cwd: bootRoot }) },
686
690
  codex: { discover: () => discoverCodexModels({ cwd: bootRoot }) },
687
691
  opencode: { discover: () => discoverOpencodeModels({ cwd: bootRoot }) },
692
+ // No `cwd`: pi's catalog is its per-user config, not a project-local answer, so this one
693
+ // adapter is correct for every project the workspace holds (#152).
694
+ pi: { discover: () => discoverPiModels() },
688
695
  },
689
696
  });
690
697
  const providerAuth = deps.providerAuth ?? new ProviderAuthService();
@@ -788,7 +795,10 @@ export function createApp(deps) {
788
795
  };
789
796
  // Hosted-mode gate (spec §"Deployment modes") — read per request so
790
797
  // XEZ_REMOTE flips take effect live (and tests can toggle it).
791
- const capabilities = () => resolveCapabilities(process.env, bindHost);
798
+ // The stored Inbox setting (F) wins over `XEZ_FOLLOWUPS` when the workspace config sets
799
+ // one. Read through the shared semaphore's cache so it is live: `refresh()` re-reads it on
800
+ // every workspace-config PUT, which is the one reload hook this file already fires.
801
+ const capabilities = () => resolveCapabilities(process.env, bindHost, deps.semaphore?.storedFollowups());
792
802
  const singleProjectRefusal = (action) => ({ error: `single-project mode is enabled; ${action} is disabled` });
793
803
  // Inbox live updates (spec 007). Opt-in (#471): no capability, no watcher —
794
804
  // and since step 2.3 the per-dataDir watch is created lazily by the first
@@ -965,6 +975,8 @@ export function createApp(deps) {
965
975
  c.set('project', await contexts.context(raw));
966
976
  }
967
977
  catch (err) {
978
+ if (err instanceof ProjectWriterError)
979
+ return c.json({ error: err.message }, 409);
968
980
  if (err instanceof ProjectContextError) {
969
981
  return err.reason === 'missing-root'
970
982
  ? c.json({ error: `project folder not found: ${err.projectId}` }, 409)
@@ -1247,9 +1259,13 @@ export function createApp(deps) {
1247
1259
  // ---- chained family: host model catalog (workspace-level) ----
1248
1260
  const modelsRoutes = new Hono()
1249
1261
  // `modelDiscoveryRunnerSchema` is the contract's own list of the runners with an
1250
- // authoritative host-local catalog (#794, #784), so the client compiles against exactly what
1251
- // this validates. A runner absent from it has no discovery path and this 400s.
1252
- .get('/models', queryZodValidator(z.object({ runner: z.union([z.string(), z.array(z.string()).transform((v) => v[0])]).pipe(modelDiscoveryRunnerSchema) }), { message: 'runner must be claude, codex or opencode' }), async (c) => {
1262
+ // authoritative host-local catalog (#794, #784, #152), so the client compiles against exactly
1263
+ // what this validates. A runner absent from it has no discovery path and this 400s.
1264
+ //
1265
+ // The message is DERIVED from that list, never spelled out. It used to name the three runners
1266
+ // literally, so adding a fourth here would have left it refusing `pi` by a name it no longer
1267
+ // rejected — and a refusal that names the wrong set is worse than a generic one (#152).
1268
+ .get('/models', queryZodValidator(z.object({ runner: z.union([z.string(), z.array(z.string()).transform((v) => v[0])]).pipe(modelDiscoveryRunnerSchema) }), { message: `runner must be one of: ${MODEL_DISCOVERY_RUNNERS.join(', ')}` }), async (c) => {
1253
1269
  const query = { data: c.req.valid('query') };
1254
1270
  return c.json(await modelCatalog.get(query.data.runner));
1255
1271
  });
@@ -2351,6 +2367,17 @@ export function createApp(deps) {
2351
2367
  projectsDir: config.projectsDir,
2352
2368
  skillsAutoUpdate: config.skillsAutoUpdate ?? null,
2353
2369
  effectiveSkillsAutoUpdate: effectiveSkillsAutoUpdate(config),
2370
+ // Tri-state (F): `null` means no stored key, so `XEZ_FOLLOWUPS` decides. The
2371
+ // `effective*` twin is what the pane renders, so "(default)" and the actual state can
2372
+ // both be shown without the client re-implementing the precedence rule.
2373
+ followups: config.followups ?? null,
2374
+ effectiveFollowups: config.followups ?? process.env.XEZ_FOLLOWUPS === '1',
2375
+ agentEnvPassthrough: config.agentEnvPassthrough ?? null,
2376
+ effectiveAgentEnvPassthrough: config.agentEnvPassthrough ??
2377
+ (process.env.XEZ_ENV_PASSTHROUGH ?? '')
2378
+ .split(',')
2379
+ .map((name) => name.trim())
2380
+ .filter(Boolean),
2354
2381
  composerDefaults: {
2355
2382
  autonomous: config.composerDefaults.autonomous ?? null,
2356
2383
  worktree: config.composerDefaults.worktree ?? null,
@@ -2366,7 +2393,11 @@ export function createApp(deps) {
2366
2393
  maxMonitoringSessions: config.resources.maxMonitoringSessions,
2367
2394
  monitoringWakeIntervalMinutes: config.resources.monitoringWakeIntervalMinutes,
2368
2395
  autoResumeOnUsageLimit: config.resources.autoResumeOnUsageLimit,
2396
+ idleTimeoutMinutes: config.resources.idleTimeoutMinutes,
2369
2397
  memoryLimitMb: config.resources.memoryLimitMb,
2398
+ // Reported, not stored: the settings pane names this machine's derived ceiling so the
2399
+ // user can see what an unset `memoryLimitMb` actually means here (B1).
2400
+ memoryLimitDefaultMb: DEFAULT_MEMORY_LIMIT_MB,
2370
2401
  worktreeRetentionDefault: config.resources.worktreeRetentionDefault,
2371
2402
  },
2372
2403
  // SPREAD, never `runner: maybeUndefined`: hono would type the key as always-present while
@@ -2382,7 +2413,7 @@ export function createApp(deps) {
2382
2413
  .get('/workspace/config', async (c) => c.json(workspaceConfigBody(await loadWorkspaceConfig())))
2383
2414
  .put('/workspace/config', jsonZodValidator(() => workspaceConfigUpdateSchema), async (c) => {
2384
2415
  const parsed = { data: c.req.valid('json') };
2385
- const { browseRoot, projectsDir, skillsAutoUpdate, composerDefaults, resources, agentDefaults } = parsed.data;
2416
+ const { browseRoot, projectsDir, skillsAutoUpdate, followups, agentEnvPassthrough, composerDefaults, resources, agentDefaults, } = parsed.data;
2386
2417
  for (const [configuredRoot, create] of [
2387
2418
  [browseRoot, false],
2388
2419
  [projectsDir, true],
@@ -2425,6 +2456,16 @@ export function createApp(deps) {
2425
2456
  delete config.skillsAutoUpdate;
2426
2457
  else if (skillsAutoUpdate !== undefined)
2427
2458
  config.skillsAutoUpdate = skillsAutoUpdate;
2459
+ // `null` clears the key back to the env default; `false`/`[]` are real stored
2460
+ // choices and must survive as written (F).
2461
+ if (followups === null)
2462
+ delete config.followups;
2463
+ else if (followups !== undefined)
2464
+ config.followups = followups;
2465
+ if (agentEnvPassthrough === null)
2466
+ delete config.agentEnvPassthrough;
2467
+ else if (agentEnvPassthrough !== undefined)
2468
+ config.agentEnvPassthrough = agentEnvPassthrough;
2428
2469
  if (composerDefaults?.autonomous === null)
2429
2470
  delete config.composerDefaults.autonomous;
2430
2471
  else if (composerDefaults?.autonomous !== undefined) {
@@ -2446,6 +2487,9 @@ export function createApp(deps) {
2446
2487
  if (resources?.autoResumeOnUsageLimit !== undefined) {
2447
2488
  config.resources.autoResumeOnUsageLimit = resources.autoResumeOnUsageLimit;
2448
2489
  }
2490
+ if (resources?.idleTimeoutMinutes !== undefined) {
2491
+ config.resources.idleTimeoutMinutes = resources.idleTimeoutMinutes;
2492
+ }
2449
2493
  if (resources?.memoryLimitMb !== undefined)
2450
2494
  config.resources.memoryLimitMb = resources.memoryLimitMb;
2451
2495
  if (resources?.worktreeRetentionDefault !== undefined) {
@@ -2480,8 +2524,11 @@ export function createApp(deps) {
2480
2524
  }
2481
2525
  // A resource change takes effect WITHOUT a restart: refresh the shared
2482
2526
  // semaphore's in-memory snapshot and pump every manager (step 2.5's hook).
2483
- if (resources !== undefined)
2527
+ // `followups` and `agentEnvPassthrough` are cached by the same semaphore snapshot
2528
+ // (F), so they refresh through the same hook rather than a second reload path.
2529
+ if (resources !== undefined || followups !== undefined || agentEnvPassthrough !== undefined) {
2484
2530
  await deps.semaphore?.refresh();
2531
+ }
2485
2532
  return c.json(workspaceConfigBody(written));
2486
2533
  })
2487
2534
  // Global GUI state (`~/.xezar/ui-state.json`) — same parse/key-cap/shallow-
@@ -2514,6 +2561,12 @@ export function createApp(deps) {
2514
2561
  browseRoot: z.string().trim().min(1).max(4096).optional(),
2515
2562
  projectsDir: z.string().trim().min(1).max(4096).optional(),
2516
2563
  skillsAutoUpdate: z.boolean().nullable().optional(),
2564
+ followups: z.boolean().nullable().optional(),
2565
+ agentEnvPassthrough: z
2566
+ .array(z.string().trim().min(1).max(200))
2567
+ .max(64)
2568
+ .nullable()
2569
+ .optional(),
2517
2570
  composerDefaults: z
2518
2571
  .object({
2519
2572
  autonomous: z.boolean().nullable().optional(),
@@ -2526,6 +2579,7 @@ export function createApp(deps) {
2526
2579
  maxMonitoringSessions: z.number().int().min(0).max(16).optional(),
2527
2580
  monitoringWakeIntervalMinutes: z.number().int().min(1).max(60).nullable().optional(),
2528
2581
  autoResumeOnUsageLimit: z.boolean().optional(),
2582
+ idleTimeoutMinutes: z.number().int().min(1).max(1440).nullable().optional(),
2529
2583
  memoryLimitMb: z.number().int().min(0).max(1_048_576).nullable().optional(),
2530
2584
  worktreeRetentionDefault: z.number().int().min(0).max(1000).optional(),
2531
2585
  })
@@ -3165,6 +3219,8 @@ export function createApp(deps) {
3165
3219
  catch (error) {
3166
3220
  if (error instanceof HistoryCursorError)
3167
3221
  return c.json({ error: error.message }, error.status);
3222
+ if (error instanceof EventCorrectionError)
3223
+ return c.json({ error: error.message }, 409);
3168
3224
  throw error;
3169
3225
  }
3170
3226
  })
@@ -3173,7 +3229,14 @@ export function createApp(deps) {
3173
3229
  const { id } = c.req.valid('param');
3174
3230
  if (!store.getRun(id))
3175
3231
  return c.json({ error: 'not found' }, 404);
3176
- return c.json(await deriveRunContextEvents(join(dataDir, 'runs', `${id}.ndjson`)));
3232
+ try {
3233
+ return c.json(await deriveRunContextEvents(join(dataDir, 'runs', `${id}.ndjson`)));
3234
+ }
3235
+ catch (error) {
3236
+ if (error instanceof EventCorrectionError)
3237
+ return c.json({ error: error.message }, 409);
3238
+ throw error;
3239
+ }
3177
3240
  })
3178
3241
  // Editable titles (#389). The UI displays `titleSummary ?? title`, so a
3179
3242
  // user edit sets BOTH: `title` (the record's own name — the raw task stops
@@ -4103,11 +4166,25 @@ export function createApp(deps) {
4103
4166
  catch (error) {
4104
4167
  if (error instanceof HistoryCursorError)
4105
4168
  return c.json({ error: error.message }, error.status);
4169
+ if (error instanceof EventCorrectionError)
4170
+ return c.json({ error: error.message }, 409);
4106
4171
  throw error;
4107
4172
  }
4108
4173
  }
4109
4174
  const lastEventId = Number.parseInt(c.req.header('Last-Event-ID') ?? '', 10);
4110
4175
  const requestedAfter = Math.max(query.afterSeq ?? 0, Number.isSafeInteger(lastEventId) && lastEventId >= 0 ? lastEventId : 0);
4176
+ if (!query.cursor) {
4177
+ try {
4178
+ validateLegacyHistoryResume(eventsPath, requestedAfter);
4179
+ }
4180
+ catch (error) {
4181
+ if (error instanceof HistoryCursorError)
4182
+ return c.json({ error: error.message }, error.status);
4183
+ if (error instanceof EventCorrectionError)
4184
+ return c.json({ error: error.message }, 409);
4185
+ throw error;
4186
+ }
4187
+ }
4111
4188
  return streamSSENoBuffer(c, async (stream) => {
4112
4189
  let replaying = true;
4113
4190
  let maxSeq = requestedAfter;
@@ -4599,6 +4676,13 @@ export function createApp(deps) {
4599
4676
  // Optional review gate (#489): tri-state — null means "no config key, the
4600
4677
  // XEZ_REVIEW_GATE env default (OFF) decides".
4601
4678
  reviewGate: config.reviewGate ?? null,
4679
+ // Planner/namer models and team skill sources (E). Declared in the file schema since
4680
+ // spec 008 but absent from this answer and from `setConfigSchema`, so the only way to
4681
+ // change them was to hand-edit `.xezar/config.json`. All three are `.default()`ed by
4682
+ // the file schema, so they are always materialized here — never tri-state.
4683
+ plannerModel: config.plannerModel,
4684
+ namerModel: config.namerModel,
4685
+ skillsRepos: config.skillsRepos,
4602
4686
  };
4603
4687
  };
4604
4688
  // ---- chained family: per-repo config (project-scoped) ----
@@ -4672,6 +4756,26 @@ export function createApp(deps) {
4672
4756
  raw.memoryLimitMb = parsed.data.memoryLimitMb;
4673
4757
  }
4674
4758
  }
4759
+ if (parsed.data.plannerModel !== undefined) {
4760
+ if (parsed.data.plannerModel === null)
4761
+ delete raw.plannerModel;
4762
+ else
4763
+ raw.plannerModel = parsed.data.plannerModel;
4764
+ }
4765
+ if (parsed.data.namerModel !== undefined) {
4766
+ if (parsed.data.namerModel === null)
4767
+ delete raw.namerModel;
4768
+ else
4769
+ raw.namerModel = parsed.data.namerModel;
4770
+ }
4771
+ if (parsed.data.skillsRepos !== undefined) {
4772
+ // `null` clears back to the default catalog; `[]` is stored, because an empty list
4773
+ // is how a repo turns team skills OFF (`gatedSkillsRepos` reads the key's presence).
4774
+ if (parsed.data.skillsRepos === null)
4775
+ delete raw.skillsRepos;
4776
+ else
4777
+ raw.skillsRepos = parsed.data.skillsRepos;
4778
+ }
4675
4779
  if (parsed.data.defaultModels !== undefined) {
4676
4780
  // Per-runner merge, so setting codex's preset never clobbers claude's.
4677
4781
  const current = raw.defaultModels && typeof raw.defaultModels === 'object'
@@ -4697,6 +4801,13 @@ export function createApp(deps) {
4697
4801
  catch (err) {
4698
4802
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
4699
4803
  }
4804
+ // B2: the per-repo `memoryLimitMb` is enforced again, and enforcement reads it from
4805
+ // the semaphore's cached snapshot — so this write has to refresh that snapshot, the
4806
+ // same way `PUT /api/v1/workspace/config` and `PATCH /api/v1/projects/:id` do. Without
4807
+ // it the key would save and do nothing until the next boot, which is the exact defect
4808
+ // B2 exists to fix.
4809
+ if (parsed.data.memoryLimitMb !== undefined)
4810
+ await deps.semaphore?.refresh();
4700
4811
  // Pre-R6 answer shape ({baseBranch, defaultRunner}) + additive R6 fields.
4701
4812
  return c.json(await configAnswer(repoRoot, await loadConfig(repoRoot)));
4702
4813
  });
@@ -4732,6 +4843,19 @@ export function createApp(deps) {
4732
4843
  // Optional review gate toggle (Settings → Agents, #489): null clears the key
4733
4844
  // back to the env-default behavior (OFF).
4734
4845
  reviewGate: z.boolean().nullable().optional(),
4846
+ // Planner/namer models and team skill sources (E). `null` clears each key back to its
4847
+ // schema default; `skillsRepos: []` is a real value that disables team skills, which is
4848
+ // why the empty array is stored rather than treated as a clear.
4849
+ plannerModel: z.string().trim().min(1).max(200).nullable().optional(),
4850
+ namerModel: z.string().trim().min(1).max(200).nullable().optional(),
4851
+ skillsRepos: z
4852
+ .array(z.object({
4853
+ repo: z.string().trim().min(1).max(500),
4854
+ ref: z.string().trim().min(1).max(200).optional(),
4855
+ }))
4856
+ .max(32)
4857
+ .nullable()
4858
+ .optional(),
4735
4859
  });
4736
4860
  const setAgentConfigSchema = z.object({
4737
4861
  content: z.string().max(2_000_000),
@@ -4849,8 +4973,38 @@ export function createApp(deps) {
4849
4973
  }
4850
4974
  return numbers;
4851
4975
  };
4852
- const runIndexEntry = (projectId, run) => {
4976
+ /**
4977
+ * How many DISTINCT backends a run's recorded steps used. A step that never ran recorded no
4978
+ * backend and is not counted — absent is "nothing happened here", never a second backend.
4979
+ *
4980
+ * Derived HERE because the index row carries no `steps[]` by design (see the schema's header).
4981
+ * The cockpit's `stepBackendCount` applies the identical rule to the fat record on the
4982
+ * per-project table; keep the two the same.
4983
+ */
4984
+ const distinctStepBackends = (run) => new Set(run.steps.flatMap((step) => (step.backend ? [step.backend] : []))).size;
4985
+ /**
4986
+ * The runner a run RAN AS, in order of how good the evidence is — `undefined` only when the run
4987
+ * carries none, which means it never started.
4988
+ *
4989
+ * `RunRecord.runner` is not "what the caller asked for": `execute` writes the resolved backend
4990
+ * onto the record the moment a run begins (`workflows/run.ts`), so for anything that ever ran it
4991
+ * is a fact. A record written before run-level backend affinity existed still has step backends,
4992
+ * stamped at spawn — and resolving one of THOSE against today's `defaultRunner` would name a
4993
+ * backend the run never touched.
4994
+ */
4995
+ const recordedRunner = (run) => {
4996
+ if (run.runner)
4997
+ return run.runner;
4998
+ for (let index = run.steps.length - 1; index >= 0; index -= 1) {
4999
+ const backend = run.steps[index]?.backend;
5000
+ if (backend)
5001
+ return backend;
5002
+ }
5003
+ return undefined;
5004
+ };
5005
+ const runIndexEntry = (projectId, run, runner) => {
4853
5006
  const usage = currentUsage(run.id);
5007
+ const backends = distinctStepBackends(run);
4854
5008
  return {
4855
5009
  projectId,
4856
5010
  id: run.id,
@@ -4865,6 +5019,15 @@ export function createApp(deps) {
4865
5019
  archived: run.archived,
4866
5020
  ...(run.autoResumeAt !== undefined ? { autoResumeAt: run.autoResumeAt } : {}),
4867
5021
  workflow: run.workflow,
5022
+ // Resolved HERE because rows span projects: the browser would need one config request per
5023
+ // project to finish it. `inherited` marks the one case that is not history — a run carrying
5024
+ // no evidence at all has not started, so the value is what it WOULD run as.
5025
+ runner: runner.runner,
5026
+ ...(runner.inherited ? { runnerInherited: true } : {}),
5027
+ ...(run.model !== undefined ? { model: run.model } : {}),
5028
+ // Only when it says something: 0 or 1 backend is every ordinary run, and a key nobody reads
5029
+ // is exactly the weight this slim row exists to refuse.
5030
+ ...(backends > 1 ? { stepBackends: backends } : {}),
4868
5031
  ...(run.branch !== undefined ? { branch: run.branch } : {}),
4869
5032
  ...(run.startedAt !== undefined ? { startedAt: run.startedAt } : {}),
4870
5033
  // The tracker-reference inputs, verbatim — the cockpit's `taskReference()` owns the rule
@@ -4933,9 +5096,30 @@ export function createApp(deps) {
4933
5096
  const recent = (owned ? owned.store.listRuns() : readRunIndexFromDisk(projectDataDir(project.root))).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
4934
5097
  if (recent.length > RUNS_INDEX_PER_PROJECT)
4935
5098
  truncated.push(project.id);
5099
+ // LAZY, and at most once per project. This route is polled every 15s and invalidated on a
5100
+ // 400ms debounce during a run, so a config read per project per request would be dozens of
5101
+ // file reads a second on a large registry. Almost every row answers for itself — a started
5102
+ // run carries its own resolved `runner` — so in practice this never runs at all.
5103
+ let projectDefault;
5104
+ const defaultRunnerFor = async () => {
5105
+ if (projectDefault === undefined) {
5106
+ try {
5107
+ projectDefault = (await loadConfig(project.root)).defaultRunner;
5108
+ }
5109
+ catch {
5110
+ // Documented contract: this read degrades to fewer or plainer rows, never a 500.
5111
+ projectDefault = 'claude';
5112
+ }
5113
+ }
5114
+ return projectDefault;
5115
+ };
4936
5116
  const mentioned = [];
4937
5117
  for (const run of recent.slice(0, RUNS_INDEX_PER_PROJECT)) {
4938
- runs.push(runIndexEntry(project.id, run));
5118
+ const recorded = recordedRunner(run);
5119
+ const runner = recorded
5120
+ ? { runner: recorded, inherited: false }
5121
+ : { runner: await defaultRunnerFor(), inherited: true };
5122
+ runs.push(runIndexEntry(project.id, run, runner));
4939
5123
  mentioned.push(...mentionedReferenceNumbers(run));
4940
5124
  }
4941
5125
  if (mentioned.length > 0) {