byterover-cli 3.10.3 → 3.12.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 (75) hide show
  1. package/README.md +4 -2
  2. package/dist/agent/core/domain/llm/registry.d.ts +12 -0
  3. package/dist/agent/core/domain/llm/registry.js +49 -0
  4. package/dist/agent/core/domain/llm/types.d.ts +6 -0
  5. package/dist/agent/core/interfaces/i-content-generator.d.ts +8 -0
  6. package/dist/agent/infra/llm/agent-llm-service.js +18 -6
  7. package/dist/agent/infra/llm/context/context-manager.d.ts +4 -1
  8. package/dist/agent/infra/llm/context/context-manager.js +5 -1
  9. package/dist/agent/infra/llm/generators/ai-sdk-content-generator.d.ts +13 -0
  10. package/dist/agent/infra/llm/generators/ai-sdk-content-generator.js +19 -6
  11. package/dist/agent/infra/llm/generators/ai-sdk-message-converter.js +16 -4
  12. package/dist/agent/infra/llm/generators/byterover-content-generator.d.ts +1 -0
  13. package/dist/agent/infra/llm/generators/byterover-content-generator.js +4 -1
  14. package/dist/agent/infra/llm/model-capabilities.d.ts +2 -1
  15. package/dist/agent/infra/llm/model-capabilities.js +6 -4
  16. package/dist/agent/infra/llm/providers/anthropic.js +2 -0
  17. package/dist/agent/infra/llm/providers/deepseek.d.ts +10 -0
  18. package/dist/agent/infra/llm/providers/deepseek.js +33 -0
  19. package/dist/agent/infra/llm/providers/glm-coding-plan.d.ts +9 -0
  20. package/dist/agent/infra/llm/providers/glm-coding-plan.js +32 -0
  21. package/dist/agent/infra/llm/providers/index.js +4 -0
  22. package/dist/agent/infra/llm/providers/openrouter.js +2 -0
  23. package/dist/agent/infra/tools/implementations/curate-tool.js +18 -8
  24. package/dist/oclif/commands/query.js +7 -1
  25. package/dist/oclif/lib/task-client.d.ts +9 -0
  26. package/dist/oclif/lib/task-client.js +11 -1
  27. package/dist/server/constants.d.ts +6 -0
  28. package/dist/server/constants.js +11 -0
  29. package/dist/server/core/domain/entities/provider-registry.js +26 -0
  30. package/dist/server/core/domain/entities/task-history-entry.d.ts +775 -0
  31. package/dist/server/core/domain/entities/task-history-entry.js +88 -0
  32. package/dist/server/core/domain/transport/schemas.d.ts +1403 -11
  33. package/dist/server/core/domain/transport/schemas.js +157 -6
  34. package/dist/server/core/domain/transport/task-info.d.ts +18 -0
  35. package/dist/server/core/interfaces/process/i-task-lifecycle-hook.d.ts +7 -0
  36. package/dist/server/core/interfaces/storage/i-task-history-store.d.ts +62 -0
  37. package/dist/server/core/interfaces/storage/i-task-history-store.js +1 -0
  38. package/dist/server/infra/daemon/brv-server.js +43 -18
  39. package/dist/server/infra/dream/dream-response-schemas.d.ts +24 -0
  40. package/dist/server/infra/dream/dream-response-schemas.js +7 -0
  41. package/dist/server/infra/dream/operations/consolidate.js +21 -8
  42. package/dist/server/infra/dream/operations/synthesize.js +35 -8
  43. package/dist/server/infra/http/provider-model-fetcher-registry.js +5 -0
  44. package/dist/server/infra/http/provider-model-fetchers.js +54 -27
  45. package/dist/server/infra/process/query-log-handler.d.ts +6 -0
  46. package/dist/server/infra/process/query-log-handler.js +23 -0
  47. package/dist/server/infra/process/task-history-entry-builder.d.ts +36 -0
  48. package/dist/server/infra/process/task-history-entry-builder.js +101 -0
  49. package/dist/server/infra/process/task-history-hook.d.ts +37 -0
  50. package/dist/server/infra/process/task-history-hook.js +70 -0
  51. package/dist/server/infra/process/task-history-store-cache.d.ts +25 -0
  52. package/dist/server/infra/process/task-history-store-cache.js +106 -0
  53. package/dist/server/infra/process/task-router.d.ts +72 -0
  54. package/dist/server/infra/process/task-router.js +690 -15
  55. package/dist/server/infra/process/transport-handlers.d.ts +8 -0
  56. package/dist/server/infra/process/transport-handlers.js +2 -0
  57. package/dist/server/infra/storage/file-task-history-store.d.ts +294 -0
  58. package/dist/server/infra/storage/file-task-history-store.js +912 -0
  59. package/dist/shared/transport/events/index.d.ts +5 -0
  60. package/dist/shared/transport/events/task-events.d.ts +204 -1
  61. package/dist/shared/transport/events/task-events.js +11 -0
  62. package/dist/tui/features/tasks/hooks/use-task-subscriptions.js +7 -0
  63. package/dist/tui/features/tasks/stores/tasks-store.d.ts +4 -16
  64. package/dist/tui/features/tasks/stores/tasks-store.js +7 -0
  65. package/dist/tui/types/messages.d.ts +2 -9
  66. package/dist/webui/assets/index-DyVvFoM6.css +1 -0
  67. package/dist/webui/assets/index-lr0byHh9.js +130 -0
  68. package/dist/webui/index.html +2 -2
  69. package/dist/webui/sw.js +1 -1
  70. package/dist/webui/workbox-9c191d2f.js +1 -0
  71. package/oclif.manifest.json +985 -985
  72. package/package.json +1 -1
  73. package/dist/webui/assets/index-CvcqpMYn.css +0 -1
  74. package/dist/webui/assets/index-thSZZahh.js +0 -130
  75. package/dist/webui/workbox-8c29f6e4.js +0 -1
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { z } from 'zod';
11
11
  import { QUERY_LOG_TIERS } from '../../domain/entities/query-log-entry.js';
12
+ import { TaskHistoryEntrySchema } from '../entities/task-history-entry.js';
12
13
  // ============================================================================
13
14
  // Zod Schemas for Runtime Validation (mirrors domain types)
14
15
  // ============================================================================
@@ -211,12 +212,22 @@ export const TransportTaskEventNames = {
211
212
  CANCEL: 'task:cancel',
212
213
  // Task terminal states
213
214
  CANCELLED: 'task:cancelled',
215
+ // Bulk delete terminal entries (M2.09)
216
+ CLEAR_COMPLETED: 'task:clearCompleted',
214
217
  COMPLETED: 'task:completed',
215
218
  CREATE: 'task:create',
216
219
  CREATED: 'task:created',
220
+ // Single delete (M2.09)
221
+ DELETE: 'task:delete',
222
+ // Multi delete (M2.09)
223
+ DELETE_BULK: 'task:deleteBulk',
224
+ // Broadcast on successful removal (M2.09)
225
+ DELETED: 'task:deleted',
217
226
  ERROR: 'task:error',
218
227
  // Internal (Transport → Agent)
219
228
  EXECUTE: 'task:execute',
229
+ // Single-task detail fetch (M2.09)
230
+ GET: 'task:get',
220
231
  // Snapshot query (Client → Transport)
221
232
  LIST: 'task:list',
222
233
  // Query metadata (Agent → Daemon, before task:completed)
@@ -386,6 +397,10 @@ export const TaskCreatedSchema = z.object({
386
397
  files: z.array(z.string()).optional(),
387
398
  /** Folder path for curate-folder task type */
388
399
  folderPath: z.string().optional(),
400
+ /** Active model id at task creation time */
401
+ model: z.string().optional(),
402
+ /** Active provider id at task creation time */
403
+ provider: z.string().optional(),
389
404
  /** Unique task identifier */
390
405
  taskId: z.string(),
391
406
  /** Task type */
@@ -443,7 +458,9 @@ export const TaskQueryResultEventSchema = z.object({
443
458
  })
444
459
  .optional(),
445
460
  taskId: z.string(),
446
- tier: z.custom((val) => new Set(QUERY_LOG_TIERS).has(val), { message: 'Invalid query log tier' }),
461
+ tier: z.custom((val) => new Set(QUERY_LOG_TIERS).has(val), {
462
+ message: 'Invalid query log tier',
463
+ }),
447
464
  timing: z.object({ durationMs: z.number() }),
448
465
  });
449
466
  /**
@@ -563,11 +580,38 @@ export const TaskCancelResponseSchema = z.object({
563
580
  /**
564
581
  * task:list - Snapshot of active and recently-completed tasks for a project.
565
582
  * Used by the web UI Tasks tab to populate state without replaying history.
566
- */
567
- export const TaskListRequestSchema = z.object({
568
- /** Optional project filter defaults to caller's registered project */
583
+ *
584
+ * M2.16: cursor pagination dropped; numbered pagination (page/pageSize) +
585
+ * full filter dimensions (search/provider/model/time/duration).
586
+ */
587
+ export const TaskListRequestSchema = z
588
+ .object({
589
+ /** Created at >= this epoch ms (M2.16). */
590
+ createdAfter: z.number().optional(),
591
+ /** Created at <= this epoch ms (M2.16). */
592
+ createdBefore: z.number().optional(),
593
+ /** Maximum elapsed time (ms) for terminal tasks (M2.16). */
594
+ maxDurationMs: z.number().optional(),
595
+ /** Minimum elapsed time (ms) for terminal tasks; only matches startedAt+completedAt rows (M2.16). */
596
+ minDurationMs: z.number().optional(),
597
+ /** Optional model id filter (M2.16). */
598
+ model: z.array(z.string()).optional(),
599
+ /** 1-based page index — server clamps to >= 1; defaults to 1 (M2.16). */
600
+ page: z.number().int().min(1).optional(),
601
+ /** Page size — server clamps to 1..1000; defaults to 50 (M2.16). */
602
+ pageSize: z.number().int().min(1).max(1000).optional(),
603
+ /** Optional project filter — defaults to caller's registered project. */
569
604
  projectPath: z.string().optional(),
570
- });
605
+ /** Optional provider id filter (M2.16). */
606
+ provider: z.array(z.string()).optional(),
607
+ /** Case-insensitive substring search over content + result + error.message (M2.16). */
608
+ searchText: z.string().optional(),
609
+ /** Optional status filter — return only tasks whose status matches one of these. */
610
+ status: z.array(z.enum(['cancelled', 'completed', 'created', 'error', 'started'])).optional(),
611
+ /** Optional task-type filter — e.g. ['curate'], ['query']. */
612
+ type: z.array(z.string()).optional(),
613
+ })
614
+ .strict();
571
615
  export const TaskListItemStatusSchema = z.enum(['cancelled', 'completed', 'created', 'error', 'started']);
572
616
  export const TaskListItemSchema = z.object({
573
617
  completedAt: z.number().optional(),
@@ -578,15 +622,122 @@ export const TaskListItemSchema = z.object({
578
622
  files: z.array(z.string()).optional(),
579
623
  /** Folder path for `curate-folder` tasks */
580
624
  folderPath: z.string().optional(),
625
+ /** Active model id at task creation time */
626
+ model: z.string().optional(),
581
627
  projectPath: z.string().optional(),
628
+ /** Active provider id at task creation time */
629
+ provider: z.string().optional(),
630
+ /**
631
+ * Result string. Only present for in-memory completed tasks (toListItem
632
+ * populates from TaskInfo.result). Persisted entries from the index do not
633
+ * carry result by 2-tier design — detail panel uses task:get for full text.
634
+ */
582
635
  result: z.string().optional(),
583
636
  startedAt: z.number().optional(),
584
637
  status: TaskListItemStatusSchema,
585
638
  taskId: z.string(),
586
639
  type: z.string(),
587
640
  });
588
- export const TaskListResponseSchema = z.object({
641
+ /** Status histogram used by FE filter-bar breakdown (M2.16). */
642
+ export const TaskListCountsSchema = z.object({
643
+ all: z.number().int().nonnegative(),
644
+ cancelled: z.number().int().nonnegative(),
645
+ completed: z.number().int().nonnegative(),
646
+ /** Tasks with status === 'error'. */
647
+ failed: z.number().int().nonnegative(),
648
+ /** Tasks with status === 'created' || 'started'. */
649
+ running: z.number().int().nonnegative(),
650
+ });
651
+ /** (providerId, modelId) pair from history (M2.16). */
652
+ export const TaskListAvailableModelSchema = z.object({
653
+ modelId: z.string(),
654
+ providerId: z.string(),
655
+ });
656
+ export const TaskListResponseSchema = z
657
+ .object({
658
+ /** Distinct (providerId, modelId) pairs in candidate set. History-derived. */
659
+ availableModels: z.array(TaskListAvailableModelSchema),
660
+ /** Distinct providerId values in candidate set. History-derived (includes uninstalled). */
661
+ availableProviders: z.array(z.string()),
662
+ /**
663
+ * Status histogram matching current filter scope (Model A — post-filter,
664
+ * `counts.all === total` invariant). FE filter-bar chip count = visible
665
+ * row count.
666
+ */
667
+ counts: TaskListCountsSchema,
668
+ /**
669
+ * 1-based page index, echoed back as-sent. Server clamps lower bound only
670
+ * (page < 1 → 1). NOT clamped against `pageCount`: a request for `page=9999`
671
+ * against a 1-page result returns `{page: 9999, tasks: []}` so the caller
672
+ * can detect an out-of-range page and correct itself.
673
+ */
674
+ page: z.number().int().min(1),
675
+ /** Total page count = max(ceil(total/pageSize), 1). */
676
+ pageCount: z.number().int().min(1),
677
+ /** Page size echoed back, clamped to [1, 1000]. */
678
+ pageSize: z.number().int().min(1).max(1000),
679
+ /** Page slice of items after all filters. */
589
680
  tasks: z.array(TaskListItemSchema),
681
+ /** Total count of items matching ALL filters (incl. status). */
682
+ total: z.number().int().nonnegative(),
683
+ })
684
+ .strict();
685
+ /**
686
+ * task:get — fetch full Level 2 detail for a single persisted task.
687
+ * Returns null when the task is unknown or its data file is corrupt.
688
+ */
689
+ export const TaskGetRequestSchema = z.object({
690
+ taskId: z.string(),
691
+ });
692
+ export const TaskGetResponseSchema = z.object({
693
+ task: TaskHistoryEntrySchema.nullable(),
694
+ });
695
+ /**
696
+ * task:delete — remove a single task from the per-project history store.
697
+ * Idempotent: deleting a non-existent task returns success: true.
698
+ */
699
+ export const TaskDeleteRequestSchema = z.object({
700
+ taskId: z.string(),
701
+ });
702
+ export const TaskDeleteResponseSchema = z.object({
703
+ error: z.string().optional(),
704
+ /**
705
+ * `true` when the task was actually removed (was live in-memory or persisted),
706
+ * `false` when the call was a no-op (taskId unknown or already tombstoned).
707
+ * Idempotent semantics on `success` are preserved — `success: true` indicates
708
+ * the request was valid; `removed` distinguishes "actually removed" from
709
+ * "no-op". `task:deleteBulk` uses this to compute an accurate `deletedCount`.
710
+ */
711
+ removed: z.boolean().optional(),
712
+ success: z.boolean(),
713
+ });
714
+ /**
715
+ * task:deleteBulk — delete many tasks at once. `deletedCount` reports actual removals.
716
+ */
717
+ export const TaskDeleteBulkRequestSchema = z.object({
718
+ taskIds: z.array(z.string()),
719
+ });
720
+ export const TaskDeleteBulkResponseSchema = z.object({
721
+ deletedCount: z.number(),
722
+ error: z.string().optional(),
723
+ });
724
+ /**
725
+ * task:clearCompleted — remove all terminal-state tasks (completed/error/cancelled)
726
+ * from the project's history. Active tasks (created/started) are preserved.
727
+ */
728
+ export const TaskClearCompletedRequestSchema = z.object({
729
+ projectPath: z.string().optional(),
730
+ });
731
+ export const TaskClearCompletedResponseSchema = z.object({
732
+ deletedCount: z.number(),
733
+ error: z.string().optional(),
734
+ });
735
+ /**
736
+ * task:deleted — broadcast to project room when a task is removed from history.
737
+ * Lets other clients (TUI, other webui tabs) drop the row from their local view.
738
+ */
739
+ export const TaskDeletedEventSchema = z.object({
740
+ taskId: z.string(),
590
741
  });
591
742
  // ============================================================================
592
743
  // Session Schemas (client → server commands)
@@ -1,7 +1,13 @@
1
+ import type { ReasoningContentItem, ToolCallEvent } from '../../../../shared/transport/events/task-events.js';
1
2
  import type { TaskErrorData, TaskListItemStatus, TaskType } from './schemas.js';
2
3
  /**
3
4
  * Tracked task metadata used by TaskRouter for routing events
4
5
  * and by ConnectionCoordinator for agent disconnect cleanup.
6
+ *
7
+ * Level 2 fields (`responseContent`, `reasoningContents`, `toolCalls`,
8
+ * `sessionId`) are accumulated from `llmservice:*` events and persisted
9
+ * by `TaskHistoryHook` so a tab refresh mid-stream can render the in-flight
10
+ * state.
5
11
  */
6
12
  export type TaskInfo = {
7
13
  /** Client's working directory for file validation */
@@ -18,8 +24,16 @@ export type TaskInfo = {
18
24
  folderPath?: string;
19
25
  /** Log entry ID set by lifecycle hook after onTaskCreate */
20
26
  logId?: string;
27
+ /** Active model id at task creation time */
28
+ model?: string;
21
29
  /** Project path this task belongs to (for multi-project routing) */
22
30
  projectPath?: string;
31
+ /** Active provider id at task creation time */
32
+ provider?: string;
33
+ /** Accumulated reasoning/thinking entries from `llmservice:thinking` + `llmservice:chunk` (type=reasoning). */
34
+ reasoningContents?: ReasoningContentItem[];
35
+ /** Final assistant response set by `llmservice:response` (overwrites on multi-step). */
36
+ responseContent?: string;
23
37
  /** Set on successful completion */
24
38
  result?: string;
25
39
  /**
@@ -29,11 +43,15 @@ export type TaskInfo = {
29
43
  * the user toggles the flag mid-task.
30
44
  */
31
45
  reviewDisabled?: boolean;
46
+ /** LLM session id set alongside `responseContent` */
47
+ sessionId?: string;
32
48
  /** Set when agent picks up the task */
33
49
  startedAt?: number;
34
50
  /** Lifecycle status — defaults to 'created' on construction */
35
51
  status?: TaskListItemStatus;
36
52
  taskId: string;
53
+ /** Accumulated tool-call lifecycle entries from `llmservice:toolCall` + `:toolResult`. */
54
+ toolCalls?: ToolCallEvent[];
37
55
  type: TaskType;
38
56
  /** Workspace root (linked subdir or projectRoot if unlinked) */
39
57
  worktreeRoot?: string;
@@ -28,6 +28,13 @@ export interface ITaskLifecycleHook {
28
28
  }>;
29
29
  /** Called when a task fails with an error. */
30
30
  onTaskError?(taskId: string, errorMessage: string, task: TaskInfo): Promise<void>;
31
+ /**
32
+ * Called by the throttled flush (~100ms window) for in-flight task mutations
33
+ * — `task:started` transition + every `llmservice:*` accumulator update.
34
+ * Optional: existing handlers (CurateLogHandler, QueryLogHandler) don't
35
+ * implement it. Implementations must never throw.
36
+ */
37
+ onTaskUpdate?(task: TaskInfo): Promise<void>;
31
38
  /** Called when an LLM tool result event is received for an ACTIVE task (not grace-period). */
32
39
  onToolResult?(taskId: string, payload: LlmToolResultEvent): void;
33
40
  }
@@ -0,0 +1,62 @@
1
+ import type { TaskListItem } from '../../../../shared/transport/events/task-events.js';
2
+ import type { TaskHistoryEntry, TaskHistoryStatus } from '../../domain/entities/task-history-entry.js';
3
+ export type { TaskHistoryEntry, TaskHistoryStatus } from '../../domain/entities/task-history-entry.js';
4
+ export interface ITaskHistoryStore {
5
+ /**
6
+ * Tombstone all matching entries. Defaults to terminal statuses
7
+ * (`'cancelled' | 'completed' | 'error'`) when `statuses` is omitted,
8
+ * so active tasks are preserved.
9
+ * Returns the list of removed taskIds (caller broadcasts `task:deleted` per id).
10
+ */
11
+ clear(options?: {
12
+ projectPath?: string;
13
+ statuses?: TaskHistoryStatus[];
14
+ }): Promise<{
15
+ deletedCount: number;
16
+ taskIds: string[];
17
+ }>;
18
+ /** Remove a single entry by taskId. Idempotent — returns false on missing/already-deleted. */
19
+ delete(taskId: string): Promise<boolean>;
20
+ /**
21
+ * Bulk-delete by taskIds. Returns the subset of input ids that were live
22
+ * (and have now been tombstoned) — invalid, unknown, and already-tombstoned
23
+ * ids are dropped. Callers can rely on the returned array length as the
24
+ * `deletedCount` and on the array contents for per-id broadcasts.
25
+ */
26
+ deleteMany(taskIds: string[]): Promise<string[]>;
27
+ /** Retrieve an entry's full Level 2 detail by taskId. Returns undefined if not found or corrupt. */
28
+ getById(taskId: string): Promise<TaskHistoryEntry | undefined>;
29
+ /**
30
+ * List entries (summary shape) sorted newest-first.
31
+ *
32
+ * Returns the wire-friendly `TaskListItem` shape — no `responseContent`, `toolCalls`,
33
+ * `reasoningContents`, `sessionId`, or `result`. Callers fetch full detail via `getById`.
34
+ *
35
+ * M2.16: param names align with the wire schema (`createdAfter` / `createdBefore`
36
+ * instead of legacy `after` / `before`). Pagination moved to the handler — store
37
+ * returns ALL matches; no `limit`.
38
+ *
39
+ * Note on `provider` / `model` / `status` filters: `handleTaskList` does NOT push
40
+ * these down — pivot filters run at the handler so derivative sets (counts,
41
+ * availableProviders, availableModels) can apply their exclusion rules. The
42
+ * options remain on the interface for direct store callers (tests, future CLI
43
+ * commands like `brv query-log`-style scans) that don't need pivot semantics.
44
+ */
45
+ list(options?: {
46
+ /** Include only entries with createdAt >= this epoch ms. */
47
+ createdAfter?: number;
48
+ /** Include only entries with createdAt <= this epoch ms. */
49
+ createdBefore?: number;
50
+ /** Include only entries matching these model ids. Direct-caller use only. */
51
+ model?: string[];
52
+ projectPath?: string;
53
+ /** Include only entries matching these provider ids. Direct-caller use only. */
54
+ provider?: string[];
55
+ /** Include only entries matching these statuses. Direct-caller use only. */
56
+ status?: TaskHistoryStatus[];
57
+ /** Include only entries matching these task types. */
58
+ type?: string[];
59
+ }): Promise<TaskListItem[]>;
60
+ /** Persist (create or overwrite) a history entry. Validates with Zod — throws on invalid shape. */
61
+ save(entry: TaskHistoryEntry): Promise<void>;
62
+ }
@@ -44,6 +44,8 @@ import { broadcastToProjectRoom } from '../process/broadcast-utils.js';
44
44
  import { CurateLogHandler } from '../process/curate-log-handler.js';
45
45
  import { setupFeatureHandlers } from '../process/feature-handlers.js';
46
46
  import { QueryLogHandler } from '../process/query-log-handler.js';
47
+ import { TaskHistoryHook } from '../process/task-history-hook.js';
48
+ import { getStore as getTaskHistoryStore } from '../process/task-history-store-cache.js';
47
49
  import { TransportHandlers } from '../process/transport-handlers.js';
48
50
  import { ProjectRegistry } from '../project/project-registry.js';
49
51
  import { createProviderOAuthTokenStore } from '../provider-oauth/provider-oauth-token-store.js';
@@ -334,6 +336,30 @@ async function main() {
334
336
  broadcastToProjectRoom(projectRegistry, projectRouter, info.projectPath, ReviewEvents.NOTIFY, payload, info.clientId);
335
337
  });
336
338
  const queryLogHandler = new QueryLogHandler();
339
+ // Task-history hook — persists every lifecycle transition + accumulated
340
+ // llmservice events to a per-project FileTaskHistoryStore. The store
341
+ // factory is module-scoped so M2.09 wire handlers can read from the
342
+ // same instances this hook writes to.
343
+ const taskHistoryHook = new TaskHistoryHook({ getStore: getTaskHistoryStore });
344
+ // Provider config/keychain stores — shared between feature handlers and state endpoint.
345
+ // Hoisted ahead of `new TransportHandlers` so the resolveActiveProvider callback below
346
+ // can close over them and call resolveProviderConfig synchronously at task-create time.
347
+ const providerConfigStore = new FileProviderConfigStore();
348
+ const providerKeychainStore = createProviderKeychainStore();
349
+ const providerOAuthTokenStore = createProviderOAuthTokenStore();
350
+ // Token refresh manager — transparently refreshes OAuth tokens before they expire
351
+ const tokenRefreshManager = new TokenRefreshManager({
352
+ providerConfigStore,
353
+ providerKeychainStore,
354
+ providerOAuthTokenStore,
355
+ transport: transportServer,
356
+ });
357
+ // Clear stale provider config on startup (e.g. migration from v1 system keychain to v2 file keystore).
358
+ // If a provider is configured but its API key is no longer accessible, disconnect it so the user
359
+ // is returned to the onboarding flow rather than hitting a cryptic API key error mid-task.
360
+ await clearStaleProviderConfig(providerConfigStore, providerKeychainStore, providerOAuthTokenStore);
361
+ // State endpoint: provider config — agents request this on startup and after provider:updated
362
+ transportServer.onRequest(TransportStateEventNames.GET_PROVIDER_CONFIG, async () => resolveProviderConfig({ authStateStore, providerConfigStore, providerKeychainStore, tokenRefreshManager }));
337
363
  const transportHandlers = new TransportHandlers({
338
364
  agentPool,
339
365
  clientManager,
@@ -341,6 +367,7 @@ async function main() {
341
367
  // so peer clients (TUI / MCP) can render drift indicators without an
342
368
  // extra round-trip.
343
369
  daemonVersion: version,
370
+ getTaskHistoryStore,
344
371
  // Resolves the project's review-disabled flag once at task-create. The result
345
372
  // is stamped onto TaskInfo + TaskExecute so daemon hooks (CurateLogHandler) and
346
373
  // the agent process (curate-tool backups, dream review entries) all observe a
@@ -348,7 +375,7 @@ async function main() {
348
375
  // idle-dream dispatch above so review semantics are identical regardless of
349
376
  // dispatch source (CLI task:create vs agent-idle trigger).
350
377
  isReviewDisabled: resolveReviewDisabled,
351
- lifecycleHooks: [curateLogHandler, queryLogHandler],
378
+ lifecycleHooks: [curateLogHandler, queryLogHandler, taskHistoryHook],
352
379
  // Daemon-side gate for dream task:create — mirrors the idle-trigger pre-check
353
380
  // in this file so the CLI path (brv dream without --force) actually honors
354
381
  // gate 3 (queue). The agent-side check kept gate 3 hardcoded to skip,
@@ -369,6 +396,21 @@ async function main() {
369
396
  },
370
397
  projectRegistry,
371
398
  projectRouter,
399
+ // Stamp the active provider/model snapshot onto every created task so the
400
+ // Web UI can display which provider handled which task. Failures are
401
+ // swallowed by TaskRouter's safeResolveActiveProvider — never blocks dispatch.
402
+ async resolveActiveProvider() {
403
+ const config = await resolveProviderConfig({
404
+ authStateStore,
405
+ providerConfigStore,
406
+ providerKeychainStore,
407
+ tokenRefreshManager,
408
+ });
409
+ return {
410
+ ...(config.activeModel ? { model: config.activeModel } : {}),
411
+ ...(config.activeProvider ? { provider: config.activeProvider } : {}),
412
+ };
413
+ },
372
414
  transport: transportServer,
373
415
  });
374
416
  transportHandlers.setup();
@@ -526,23 +568,6 @@ async function main() {
526
568
  running: transportServer.isRunning(),
527
569
  },
528
570
  }));
529
- // Provider config/keychain stores — shared between feature handlers and state endpoint
530
- const providerConfigStore = new FileProviderConfigStore();
531
- const providerKeychainStore = createProviderKeychainStore();
532
- const providerOAuthTokenStore = createProviderOAuthTokenStore();
533
- // Token refresh manager — transparently refreshes OAuth tokens before they expire
534
- const tokenRefreshManager = new TokenRefreshManager({
535
- providerConfigStore,
536
- providerKeychainStore,
537
- providerOAuthTokenStore,
538
- transport: transportServer,
539
- });
540
- // Clear stale provider config on startup (e.g. migration from v1 system keychain to v2 file keystore).
541
- // If a provider is configured but its API key is no longer accessible, disconnect it so the user
542
- // is returned to the onboarding flow rather than hitting a cryptic API key error mid-task.
543
- await clearStaleProviderConfig(providerConfigStore, providerKeychainStore, providerOAuthTokenStore);
544
- // State endpoint: provider config — agents request this on startup and after provider:updated
545
- transportServer.onRequest(TransportStateEventNames.GET_PROVIDER_CONFIG, async () => resolveProviderConfig({ authStateStore, providerConfigStore, providerKeychainStore, tokenRefreshManager }));
546
571
  // Feature handlers (auth, init, status, push, pull, etc.) require async OIDC discovery.
547
572
  // Placed after daemon:getState so the debug endpoint is available immediately,
548
573
  // without waiting for OIDC discovery (~400ms).
@@ -86,10 +86,16 @@ export declare const SynthesisCandidateSchema: z.ZodObject<{
86
86
  domain: string;
87
87
  fact: string;
88
88
  }>, "many">;
89
+ keywords: z.ZodArray<z.ZodString, "many">;
89
90
  placement: z.ZodString;
91
+ summary: z.ZodString;
92
+ tags: z.ZodArray<z.ZodString, "many">;
90
93
  title: z.ZodString;
91
94
  }, "strip", z.ZodTypeAny, {
95
+ summary: string;
96
+ tags: string[];
92
97
  title: string;
98
+ keywords: string[];
93
99
  confidence: number;
94
100
  claim: string;
95
101
  evidence: {
@@ -98,7 +104,10 @@ export declare const SynthesisCandidateSchema: z.ZodObject<{
98
104
  }[];
99
105
  placement: string;
100
106
  }, {
107
+ summary: string;
108
+ tags: string[];
101
109
  title: string;
110
+ keywords: string[];
102
111
  confidence: number;
103
112
  claim: string;
104
113
  evidence: {
@@ -121,10 +130,16 @@ export declare const SynthesizeResponseSchema: z.ZodObject<{
121
130
  domain: string;
122
131
  fact: string;
123
132
  }>, "many">;
133
+ keywords: z.ZodArray<z.ZodString, "many">;
124
134
  placement: z.ZodString;
135
+ summary: z.ZodString;
136
+ tags: z.ZodArray<z.ZodString, "many">;
125
137
  title: z.ZodString;
126
138
  }, "strip", z.ZodTypeAny, {
139
+ summary: string;
140
+ tags: string[];
127
141
  title: string;
142
+ keywords: string[];
128
143
  confidence: number;
129
144
  claim: string;
130
145
  evidence: {
@@ -133,7 +148,10 @@ export declare const SynthesizeResponseSchema: z.ZodObject<{
133
148
  }[];
134
149
  placement: string;
135
150
  }, {
151
+ summary: string;
152
+ tags: string[];
136
153
  title: string;
154
+ keywords: string[];
137
155
  confidence: number;
138
156
  claim: string;
139
157
  evidence: {
@@ -144,7 +162,10 @@ export declare const SynthesizeResponseSchema: z.ZodObject<{
144
162
  }>, "many">;
145
163
  }, "strip", z.ZodTypeAny, {
146
164
  syntheses: {
165
+ summary: string;
166
+ tags: string[];
147
167
  title: string;
168
+ keywords: string[];
148
169
  confidence: number;
149
170
  claim: string;
150
171
  evidence: {
@@ -155,7 +176,10 @@ export declare const SynthesizeResponseSchema: z.ZodObject<{
155
176
  }[];
156
177
  }, {
157
178
  syntheses: {
179
+ summary: string;
180
+ tags: string[];
158
181
  title: string;
182
+ keywords: string[];
159
183
  confidence: number;
160
184
  claim: string;
161
185
  evidence: {
@@ -13,6 +13,10 @@ export const ConsolidateResponseSchema = z.object({
13
13
  actions: z.array(ConsolidationActionSchema),
14
14
  });
15
15
  // ── Synthesize ───────────────────────────────────────────────────────────────
16
+ // Bounds are slightly above the prompt's soft targets (200 chars / 3-5 tags /
17
+ // 5-10 keywords) so a model that goes a little over still produces a usable
18
+ // synthesis instead of being rejected outright; the caps still prevent a
19
+ // runaway model from landing oversized text directly in card-mode YAML.
16
20
  export const SynthesisCandidateSchema = z.object({
17
21
  claim: z.string(),
18
22
  confidence: z.number().min(0).max(1),
@@ -20,7 +24,10 @@ export const SynthesisCandidateSchema = z.object({
20
24
  domain: z.string(),
21
25
  fact: z.string(),
22
26
  })),
27
+ keywords: z.array(z.string()).max(15),
23
28
  placement: z.string(),
29
+ summary: z.string().max(500),
30
+ tags: z.array(z.string()).max(8),
24
31
  title: z.string(),
25
32
  });
26
33
  export const SynthesizeResponseSchema = z.object({
@@ -15,6 +15,7 @@ import { randomUUID } from 'node:crypto';
15
15
  import { access, mkdir, readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
16
16
  import { dirname, join } from 'node:path';
17
17
  import { warnSidecarFailure } from '../../../core/domain/knowledge/sidecar-logging.js';
18
+ import { isExcludedFromSync } from '../../context-tree/derived-artifact.js';
18
19
  import { ConsolidateResponseSchema } from '../dream-response-schemas.js';
19
20
  import { parseDreamResponse } from '../parse-dream-response.js';
20
21
  /**
@@ -226,7 +227,7 @@ function addFrontmatterFields(content, fields) {
226
227
  if (parsed && typeof parsed === 'object') {
227
228
  // Spread preserves existing key order; new fields are appended at end.
228
229
  const merged = { ...parsed, ...fields };
229
- const newYaml = yamlDump(merged, { flowLevel: 2, lineWidth: -1, sortKeys: false }).trimEnd();
230
+ const newYaml = yamlDump(merged, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
230
231
  return `---\n${newYaml}\n---\n${body}`;
231
232
  }
232
233
  }
@@ -236,7 +237,7 @@ function addFrontmatterFields(content, fields) {
236
237
  }
237
238
  }
238
239
  // No valid frontmatter — prepend
239
- const yaml = yamlDump(fields, { flowLevel: 2, lineWidth: -1, sortKeys: false }).trimEnd();
240
+ const yaml = yamlDump(fields, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
240
241
  return `---\n${yaml}\n---\n${content}`;
241
242
  }
242
243
  // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -458,8 +459,10 @@ async function executeCrossReference(action, ctx) {
458
459
  await Promise.all(Object.entries(previousTexts).map(([file, content]) => reviewBackupStore.save(file, content).catch(() => { })));
459
460
  }
460
461
  // For each file, add the other files to its related frontmatter
461
- await Promise.all(action.files.map((file) => {
462
- const otherFiles = action.files.filter((f) => f !== file);
462
+ // Skip derived-artifact targets so we never write related: onto them.
463
+ const eligibleFiles = action.files.filter((f) => !isExcludedFromSync(f));
464
+ await Promise.all(eligibleFiles.map((file) => {
465
+ const otherFiles = eligibleFiles.filter((f) => f !== file);
463
466
  return addRelatedLinks(join(contextTreeDir, file), otherFiles);
464
467
  }));
465
468
  return {
@@ -472,6 +475,8 @@ async function executeCrossReference(action, ctx) {
472
475
  };
473
476
  }
474
477
  async function addRelatedLinks(filePath, relatedPaths) {
478
+ // Skip paths that won't be pushed — they'd be dangling refs on remote.
479
+ const incoming = relatedPaths.filter((p) => !isExcludedFromSync(p));
475
480
  let content;
476
481
  try {
477
482
  content = await readFile(filePath, 'utf8');
@@ -491,8 +496,14 @@ async function addRelatedLinks(filePath, relatedPaths) {
491
496
  try {
492
497
  const parsed = yamlLoad(yamlBlock);
493
498
  if (parsed && typeof parsed === 'object') {
494
- const existing = Array.isArray(parsed.related) ? parsed.related : [];
495
- parsed.related = [...new Set([...existing, ...relatedPaths])];
499
+ const hadRelated = Array.isArray(parsed.related);
500
+ const existing = (Array.isArray(parsed.related) ? parsed.related : [])
501
+ .filter((p) => !isExcludedFromSync(p));
502
+ const merged = [...new Set([...existing, ...incoming])];
503
+ // Don't introduce a related: [] key into a file that didn't have one.
504
+ if (!hadRelated && merged.length === 0)
505
+ return;
506
+ parsed.related = merged;
496
507
  const newYaml = yamlDump(parsed, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
497
508
  await atomicWrite(filePath, `---\n${newYaml}\n---\n${body}`);
498
509
  return;
@@ -503,8 +514,10 @@ async function addRelatedLinks(filePath, relatedPaths) {
503
514
  }
504
515
  }
505
516
  }
506
- // No existing frontmatter — add one with related field
507
- const yaml = yamlDump({ related: relatedPaths }, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
517
+ // No existing frontmatter — add one with related field, unless filter left nothing to add.
518
+ if (incoming.length === 0)
519
+ return;
520
+ const yaml = yamlDump({ related: incoming }, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
508
521
  await atomicWrite(filePath, `---\n${yaml}\n---\n${content}`);
509
522
  }
510
523
  async function determineNeedsReview(actionType, files, opts) {