byterover-cli 3.11.0 → 3.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.
- package/.env.production +2 -1
- package/dist/agent/infra/tools/implementations/curate-tool.js +18 -8
- package/dist/oclif/commands/curate/index.js +6 -0
- package/dist/oclif/commands/providers/connect.d.ts +26 -1
- package/dist/oclif/commands/providers/connect.js +95 -17
- package/dist/oclif/commands/providers/list.d.ts +10 -1
- package/dist/oclif/commands/providers/list.js +35 -3
- package/dist/oclif/commands/query.js +6 -0
- package/dist/oclif/commands/status.js +4 -0
- package/dist/oclif/lib/billing-line.d.ts +8 -0
- package/dist/oclif/lib/billing-line.js +45 -0
- package/dist/oclif/lib/format-billing-line.d.ts +2 -0
- package/dist/oclif/lib/format-billing-line.js +19 -0
- package/dist/oclif/lib/insufficient-credits.d.ts +11 -0
- package/dist/oclif/lib/insufficient-credits.js +36 -0
- package/dist/server/config/environment.d.ts +1 -0
- package/dist/server/config/environment.js +3 -0
- package/dist/server/constants.d.ts +6 -0
- package/dist/server/constants.js +11 -0
- package/dist/server/core/domain/entities/task-history-entry.d.ts +775 -0
- package/dist/server/core/domain/entities/task-history-entry.js +88 -0
- package/dist/server/core/domain/transport/schemas.d.ts +1420 -11
- package/dist/server/core/domain/transport/schemas.js +160 -6
- package/dist/server/core/domain/transport/task-info.d.ts +18 -0
- package/dist/server/core/interfaces/process/i-task-lifecycle-hook.d.ts +7 -0
- package/dist/server/core/interfaces/services/i-billing-service.d.ts +26 -0
- package/dist/server/core/interfaces/services/i-billing-service.js +1 -0
- package/dist/server/core/interfaces/storage/i-billing-config-store.d.ts +4 -0
- package/dist/server/core/interfaces/storage/i-billing-config-store.js +1 -0
- package/dist/server/core/interfaces/storage/i-task-history-store.d.ts +62 -0
- package/dist/server/core/interfaces/storage/i-task-history-store.js +1 -0
- package/dist/server/infra/billing/billing-state-endpoint.d.ts +4 -0
- package/dist/server/infra/billing/billing-state-endpoint.js +7 -0
- package/dist/server/infra/billing/build-status-billing.d.ts +9 -0
- package/dist/server/infra/billing/build-status-billing.js +36 -0
- package/dist/server/infra/billing/http-billing-service.d.ts +19 -0
- package/dist/server/infra/billing/http-billing-service.js +57 -0
- package/dist/server/infra/billing/paid-organizations-endpoint.d.ts +8 -0
- package/dist/server/infra/billing/paid-organizations-endpoint.js +18 -0
- package/dist/server/infra/billing/resolve-billing-source.d.ts +13 -0
- package/dist/server/infra/billing/resolve-billing-source.js +36 -0
- package/dist/server/infra/billing/resolve-billing-team.d.ts +5 -0
- package/dist/server/infra/billing/resolve-billing-team.js +8 -0
- package/dist/server/infra/connectors/rules/rules-connector.js +7 -2
- package/dist/server/infra/connectors/shared/constants.d.ts +9 -0
- package/dist/server/infra/connectors/shared/constants.js +31 -5
- package/dist/server/infra/daemon/agent-process.js +10 -8
- package/dist/server/infra/daemon/brv-server.js +48 -18
- package/dist/server/infra/dream/dream-response-schemas.d.ts +24 -0
- package/dist/server/infra/dream/dream-response-schemas.js +7 -0
- package/dist/server/infra/dream/operations/consolidate.js +21 -8
- package/dist/server/infra/dream/operations/synthesize.js +35 -8
- package/dist/server/infra/http/provider-model-fetchers.js +10 -4
- package/dist/server/infra/process/feature-handlers.d.ts +3 -1
- package/dist/server/infra/process/feature-handlers.js +26 -2
- package/dist/server/infra/process/task-history-entry-builder.d.ts +36 -0
- package/dist/server/infra/process/task-history-entry-builder.js +101 -0
- package/dist/server/infra/process/task-history-hook.d.ts +37 -0
- package/dist/server/infra/process/task-history-hook.js +70 -0
- package/dist/server/infra/process/task-history-store-cache.d.ts +25 -0
- package/dist/server/infra/process/task-history-store-cache.js +106 -0
- package/dist/server/infra/process/task-router.d.ts +72 -0
- package/dist/server/infra/process/task-router.js +690 -15
- package/dist/server/infra/process/transport-handlers.d.ts +8 -0
- package/dist/server/infra/process/transport-handlers.js +2 -0
- package/dist/server/infra/storage/file-billing-config-store.d.ts +13 -0
- package/dist/server/infra/storage/file-billing-config-store.js +55 -0
- package/dist/server/infra/storage/file-task-history-store.d.ts +294 -0
- package/dist/server/infra/storage/file-task-history-store.js +912 -0
- package/dist/server/infra/transport/handlers/auth-handler.d.ts +4 -0
- package/dist/server/infra/transport/handlers/auth-handler.js +20 -2
- package/dist/server/infra/transport/handlers/billing-handler.d.ts +30 -0
- package/dist/server/infra/transport/handlers/billing-handler.js +132 -0
- package/dist/server/infra/transport/handlers/index.d.ts +4 -0
- package/dist/server/infra/transport/handlers/index.js +2 -0
- package/dist/server/infra/transport/handlers/init-handler.js +2 -0
- package/dist/server/infra/transport/handlers/status-handler.d.ts +14 -0
- package/dist/server/infra/transport/handlers/status-handler.js +16 -0
- package/dist/server/infra/transport/handlers/team-handler.d.ts +19 -0
- package/dist/server/infra/transport/handlers/team-handler.js +40 -0
- package/dist/shared/transport/events/auth-events.d.ts +3 -0
- package/dist/shared/transport/events/billing-events.d.ts +48 -0
- package/dist/shared/transport/events/billing-events.js +8 -0
- package/dist/shared/transport/events/index.d.ts +16 -0
- package/dist/shared/transport/events/index.js +6 -0
- package/dist/shared/transport/events/task-events.d.ts +204 -1
- package/dist/shared/transport/events/task-events.js +11 -0
- package/dist/shared/transport/events/team-events.d.ts +8 -0
- package/dist/shared/transport/events/team-events.js +3 -0
- package/dist/shared/transport/types/dto.d.ts +80 -0
- package/dist/tui/features/tasks/hooks/use-task-subscriptions.js +7 -0
- package/dist/tui/features/tasks/stores/tasks-store.d.ts +4 -16
- package/dist/tui/features/tasks/stores/tasks-store.js +7 -0
- package/dist/tui/types/messages.d.ts +2 -9
- package/dist/webui/assets/index-B9JmEFOK.js +130 -0
- package/dist/webui/assets/index-CMIKsBMr.css +1 -0
- package/dist/webui/index.html +2 -2
- package/dist/webui/sw.js +1 -1
- package/oclif.manifest.json +653 -645
- package/package.json +1 -1
- package/dist/webui/assets/index--sXE__bc.css +0 -1
- package/dist/webui/assets/index-Bkkx961b.js +0 -130
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a `TaskHistoryEntry` from a live `TaskInfo`. Single source of truth
|
|
3
|
+
* for `task-router.ts:handleTaskGet` (in-memory synthesis) and
|
|
4
|
+
* `task-history-hook.ts:persist` (lifecycle persistence) — extracting it here
|
|
5
|
+
* eliminates the duplicate `baseFromTaskInfo` + `statusShapeFromTaskInfo`
|
|
6
|
+
* pair that previously lived in both modules and would silently drift.
|
|
7
|
+
*
|
|
8
|
+
* Two functions are exposed:
|
|
9
|
+
* - `buildTaskHistoryEntry(task)` — full Zod-parsed `TaskHistoryEntry`,
|
|
10
|
+
* used by `task-router.handleTaskGet` to return the same shape as
|
|
11
|
+
* `store.getById` for in-flight tasks. Returns `undefined` when the
|
|
12
|
+
* `TaskInfo` is incomplete (e.g. missing `projectPath`) or when the
|
|
13
|
+
* inferred shape fails Zod validation.
|
|
14
|
+
* - `buildTaskHistoryEntryCandidate({task, override})` — pre-Zod object
|
|
15
|
+
* used by the lifecycle hook, which sometimes injects branch-specific
|
|
16
|
+
* fields (terminal completedAt / error / result) before validation.
|
|
17
|
+
*/
|
|
18
|
+
import type { TaskInfo } from '../../core/domain/transport/task-info.js';
|
|
19
|
+
import { type TaskHistoryEntry } from '../../core/domain/entities/task-history-entry.js';
|
|
20
|
+
/**
|
|
21
|
+
* Build the pre-validation candidate object. The lifecycle hook calls this
|
|
22
|
+
* with an `override` to inject terminal-status fields before Zod-parsing.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildTaskHistoryEntryCandidate(args: {
|
|
25
|
+
override?: Record<string, unknown>;
|
|
26
|
+
task: TaskInfo;
|
|
27
|
+
}): Record<string, unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* Build a fully-validated `TaskHistoryEntry` from in-memory `TaskInfo`.
|
|
30
|
+
* Returns `undefined` when `task.projectPath` is missing or when the
|
|
31
|
+
* candidate fails Zod validation.
|
|
32
|
+
*
|
|
33
|
+
* Used by `task-router.handleTaskGet` to return live in-flight tasks in the
|
|
34
|
+
* same shape `store.getById` would have returned for persisted ones.
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildTaskHistoryEntry(task: TaskInfo): TaskHistoryEntry | undefined;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a `TaskHistoryEntry` from a live `TaskInfo`. Single source of truth
|
|
3
|
+
* for `task-router.ts:handleTaskGet` (in-memory synthesis) and
|
|
4
|
+
* `task-history-hook.ts:persist` (lifecycle persistence) — extracting it here
|
|
5
|
+
* eliminates the duplicate `baseFromTaskInfo` + `statusShapeFromTaskInfo`
|
|
6
|
+
* pair that previously lived in both modules and would silently drift.
|
|
7
|
+
*
|
|
8
|
+
* Two functions are exposed:
|
|
9
|
+
* - `buildTaskHistoryEntry(task)` — full Zod-parsed `TaskHistoryEntry`,
|
|
10
|
+
* used by `task-router.handleTaskGet` to return the same shape as
|
|
11
|
+
* `store.getById` for in-flight tasks. Returns `undefined` when the
|
|
12
|
+
* `TaskInfo` is incomplete (e.g. missing `projectPath`) or when the
|
|
13
|
+
* inferred shape fails Zod validation.
|
|
14
|
+
* - `buildTaskHistoryEntryCandidate({task, override})` — pre-Zod object
|
|
15
|
+
* used by the lifecycle hook, which sometimes injects branch-specific
|
|
16
|
+
* fields (terminal completedAt / error / result) before validation.
|
|
17
|
+
*/
|
|
18
|
+
import { TASK_HISTORY_ID_PREFIX } from '../../constants.js';
|
|
19
|
+
import { TASK_HISTORY_SCHEMA_VERSION, TaskHistoryEntrySchema, } from '../../core/domain/entities/task-history-entry.js';
|
|
20
|
+
/** Build the base shape (fields shared by every status branch). */
|
|
21
|
+
function baseFromTaskInfo(task) {
|
|
22
|
+
return {
|
|
23
|
+
content: task.content,
|
|
24
|
+
createdAt: task.createdAt,
|
|
25
|
+
id: `${TASK_HISTORY_ID_PREFIX}-${task.taskId}`,
|
|
26
|
+
projectPath: task.projectPath,
|
|
27
|
+
schemaVersion: TASK_HISTORY_SCHEMA_VERSION,
|
|
28
|
+
taskId: task.taskId,
|
|
29
|
+
type: task.type,
|
|
30
|
+
...(task.clientCwd === undefined ? {} : { clientCwd: task.clientCwd }),
|
|
31
|
+
...(task.files === undefined ? {} : { files: task.files }),
|
|
32
|
+
...(task.folderPath === undefined ? {} : { folderPath: task.folderPath }),
|
|
33
|
+
...(task.logId === undefined ? {} : { logId: task.logId }),
|
|
34
|
+
...(task.model === undefined ? {} : { model: task.model }),
|
|
35
|
+
...(task.provider === undefined ? {} : { provider: task.provider }),
|
|
36
|
+
...(task.reasoningContents === undefined ? {} : { reasoningContents: task.reasoningContents }),
|
|
37
|
+
...(task.responseContent === undefined ? {} : { responseContent: task.responseContent }),
|
|
38
|
+
...(task.sessionId === undefined ? {} : { sessionId: task.sessionId }),
|
|
39
|
+
...(task.toolCalls === undefined ? {} : { toolCalls: task.toolCalls }),
|
|
40
|
+
...(task.worktreeRoot === undefined ? {} : { worktreeRoot: task.worktreeRoot }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build the per-branch shape inferred from `task.status`. Override-only
|
|
45
|
+
* paths (terminal hooks) supply their own status; this is the default for
|
|
46
|
+
* in-flight transitions.
|
|
47
|
+
*/
|
|
48
|
+
function statusShapeFromTaskInfo(task) {
|
|
49
|
+
switch (task.status) {
|
|
50
|
+
case 'cancelled':
|
|
51
|
+
case 'completed': {
|
|
52
|
+
return {
|
|
53
|
+
completedAt: task.completedAt ?? Date.now(),
|
|
54
|
+
status: task.status,
|
|
55
|
+
...(task.startedAt === undefined ? {} : { startedAt: task.startedAt }),
|
|
56
|
+
...(task.status === 'completed' && task.result !== undefined ? { result: task.result } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
case 'error': {
|
|
60
|
+
return {
|
|
61
|
+
completedAt: task.completedAt ?? Date.now(),
|
|
62
|
+
error: task.error ?? { code: 'TASK_ERROR', message: 'unknown error', name: 'TaskError' },
|
|
63
|
+
status: 'error',
|
|
64
|
+
...(task.startedAt === undefined ? {} : { startedAt: task.startedAt }),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
case 'started': {
|
|
68
|
+
return { startedAt: task.startedAt ?? task.createdAt, status: 'started' };
|
|
69
|
+
}
|
|
70
|
+
// 'created' or undefined — minimal base, no extra branch fields.
|
|
71
|
+
default: {
|
|
72
|
+
return { status: 'created' };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build the pre-validation candidate object. The lifecycle hook calls this
|
|
78
|
+
* with an `override` to inject terminal-status fields before Zod-parsing.
|
|
79
|
+
*/
|
|
80
|
+
export function buildTaskHistoryEntryCandidate(args) {
|
|
81
|
+
const { override, task } = args;
|
|
82
|
+
if (override !== undefined) {
|
|
83
|
+
return { ...baseFromTaskInfo(task), ...statusShapeFromTaskInfo(task), ...override };
|
|
84
|
+
}
|
|
85
|
+
return { ...baseFromTaskInfo(task), ...statusShapeFromTaskInfo(task) };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Build a fully-validated `TaskHistoryEntry` from in-memory `TaskInfo`.
|
|
89
|
+
* Returns `undefined` when `task.projectPath` is missing or when the
|
|
90
|
+
* candidate fails Zod validation.
|
|
91
|
+
*
|
|
92
|
+
* Used by `task-router.handleTaskGet` to return live in-flight tasks in the
|
|
93
|
+
* same shape `store.getById` would have returned for persisted ones.
|
|
94
|
+
*/
|
|
95
|
+
export function buildTaskHistoryEntry(task) {
|
|
96
|
+
if (task.projectPath === undefined)
|
|
97
|
+
return undefined;
|
|
98
|
+
const candidate = buildTaskHistoryEntryCandidate({ task });
|
|
99
|
+
const parsed = TaskHistoryEntrySchema.safeParse(candidate);
|
|
100
|
+
return parsed.success ? parsed.data : undefined;
|
|
101
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskHistoryHook — persists `TaskInfo` to `ITaskHistoryStore` at every
|
|
3
|
+
* lifecycle transition (created / started-via-throttle / terminal).
|
|
4
|
+
*
|
|
5
|
+
* Wired into TaskRouter via `lifecycleHooks[]`. The 4 existing methods fire
|
|
6
|
+
* synchronously at create + terminal; the new `onTaskUpdate` fires on the
|
|
7
|
+
* throttled flush (~100ms) for in-flight mutations populated by the
|
|
8
|
+
* llmservice accumulator.
|
|
9
|
+
*
|
|
10
|
+
* Holds NO per-task state — every method reads from the live `TaskInfo`
|
|
11
|
+
* passed in. Errors are swallowed via `processLog`; tasks without
|
|
12
|
+
* `projectPath` are skipped silently.
|
|
13
|
+
*/
|
|
14
|
+
import type { TaskInfo } from '../../core/domain/transport/task-info.js';
|
|
15
|
+
import type { ITaskLifecycleHook } from '../../core/interfaces/process/i-task-lifecycle-hook.js';
|
|
16
|
+
import type { ITaskHistoryStore } from '../../core/interfaces/storage/i-task-history-store.js';
|
|
17
|
+
type TaskHistoryHookOptions = {
|
|
18
|
+
/** Per-project store factory (DIP — never depends on FileTaskHistoryStore directly). */
|
|
19
|
+
getStore: (projectPath: string) => ITaskHistoryStore;
|
|
20
|
+
};
|
|
21
|
+
export declare class TaskHistoryHook implements ITaskLifecycleHook {
|
|
22
|
+
private readonly getStore;
|
|
23
|
+
constructor(opts: TaskHistoryHookOptions);
|
|
24
|
+
onTaskCancelled(_taskId: string, task: TaskInfo): Promise<void>;
|
|
25
|
+
onTaskCompleted(_taskId: string, result: string, task: TaskInfo): Promise<void>;
|
|
26
|
+
onTaskCreate(task: TaskInfo): Promise<void>;
|
|
27
|
+
onTaskError(_taskId: string, errorMessage: string, task: TaskInfo): Promise<void>;
|
|
28
|
+
onTaskUpdate(task: TaskInfo): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Build + save a `TaskHistoryEntry` from the current `TaskInfo`. Optional
|
|
31
|
+
* `override` injects branch-specific fields (status / completedAt / error /
|
|
32
|
+
* result). When omitted, the branch shape is inferred from `task.status`
|
|
33
|
+
* by `buildTaskHistoryEntryCandidate`.
|
|
34
|
+
*/
|
|
35
|
+
private persist;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskHistoryHook — persists `TaskInfo` to `ITaskHistoryStore` at every
|
|
3
|
+
* lifecycle transition (created / started-via-throttle / terminal).
|
|
4
|
+
*
|
|
5
|
+
* Wired into TaskRouter via `lifecycleHooks[]`. The 4 existing methods fire
|
|
6
|
+
* synchronously at create + terminal; the new `onTaskUpdate` fires on the
|
|
7
|
+
* throttled flush (~100ms) for in-flight mutations populated by the
|
|
8
|
+
* llmservice accumulator.
|
|
9
|
+
*
|
|
10
|
+
* Holds NO per-task state — every method reads from the live `TaskInfo`
|
|
11
|
+
* passed in. Errors are swallowed via `processLog`; tasks without
|
|
12
|
+
* `projectPath` are skipped silently.
|
|
13
|
+
*/
|
|
14
|
+
import { TaskHistoryEntrySchema } from '../../core/domain/entities/task-history-entry.js';
|
|
15
|
+
import { processLog } from '../../utils/process-logger.js';
|
|
16
|
+
import { buildTaskHistoryEntryCandidate } from './task-history-entry-builder.js';
|
|
17
|
+
export class TaskHistoryHook {
|
|
18
|
+
getStore;
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
this.getStore = opts.getStore;
|
|
21
|
+
}
|
|
22
|
+
async onTaskCancelled(_taskId, task) {
|
|
23
|
+
await this.persist(task, { completedAt: Date.now(), status: 'cancelled' });
|
|
24
|
+
}
|
|
25
|
+
async onTaskCompleted(_taskId, result, task) {
|
|
26
|
+
await this.persist(task, {
|
|
27
|
+
completedAt: Date.now(),
|
|
28
|
+
...(result ? { result } : {}),
|
|
29
|
+
status: 'completed',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async onTaskCreate(task) {
|
|
33
|
+
await this.persist(task, { status: 'created' });
|
|
34
|
+
}
|
|
35
|
+
async onTaskError(_taskId, errorMessage, task) {
|
|
36
|
+
await this.persist(task, {
|
|
37
|
+
completedAt: Date.now(),
|
|
38
|
+
error: { code: 'TASK_ERROR', message: errorMessage, name: 'TaskError' },
|
|
39
|
+
status: 'error',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async onTaskUpdate(task) {
|
|
43
|
+
await this.persist(task);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build + save a `TaskHistoryEntry` from the current `TaskInfo`. Optional
|
|
47
|
+
* `override` injects branch-specific fields (status / completedAt / error /
|
|
48
|
+
* result). When omitted, the branch shape is inferred from `task.status`
|
|
49
|
+
* by `buildTaskHistoryEntryCandidate`.
|
|
50
|
+
*/
|
|
51
|
+
async persist(task, override) {
|
|
52
|
+
if (!task.projectPath)
|
|
53
|
+
return;
|
|
54
|
+
const candidate = buildTaskHistoryEntryCandidate({ override, task });
|
|
55
|
+
let entry;
|
|
56
|
+
try {
|
|
57
|
+
entry = TaskHistoryEntrySchema.parse(candidate);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
processLog(`TaskHistoryHook: failed to build entry for ${task.taskId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await this.getStore(task.projectPath).save(entry);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
processLog(`TaskHistoryHook: store.save failed for ${task.taskId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project FileTaskHistoryStore cache + lazy startup audit.
|
|
3
|
+
*
|
|
4
|
+
* Module-scoped so M2.09's wire handlers can reuse the same store instances
|
|
5
|
+
* the M2.06 lifecycle hook writes to. Audit fires once per project on first
|
|
6
|
+
* access, comparing `_index.jsonl` ↔ `data/` files to flag orphans without
|
|
7
|
+
* auto-fixing (M2.03 compaction owns the cleanup pass).
|
|
8
|
+
*/
|
|
9
|
+
import { FileTaskHistoryStore } from '../storage/file-task-history-store.js';
|
|
10
|
+
/**
|
|
11
|
+
* Resolve (or lazily create) the per-project store. The first call for a
|
|
12
|
+
* given `projectPath` schedules a best-effort audit; subsequent calls reuse
|
|
13
|
+
* the cached store and skip re-auditing.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getStore(projectPath: string): FileTaskHistoryStore;
|
|
16
|
+
/**
|
|
17
|
+
* Compare `_index.jsonl` (live entries) against the `data/` directory and log
|
|
18
|
+
* orphans. Best-effort: never throws to caller. The `log` parameter defaults
|
|
19
|
+
* to `processLog` for production; tests inject a stub.
|
|
20
|
+
*/
|
|
21
|
+
export declare function auditTaskHistory(projectPath: string, store: FileTaskHistoryStore, log?: ((msg: string) => void) | undefined): Promise<void>;
|
|
22
|
+
/** Test-only: clear module-scope state so each test sees a fresh cache. */
|
|
23
|
+
export declare function resetTaskHistoryStoreCache(): void;
|
|
24
|
+
/** Test-only: inject a logger into the audit path triggered by `getStore`. Pass no arg to clear. */
|
|
25
|
+
export declare function _setTestLoggerForGetStore(log?: (msg: string) => void): void;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project FileTaskHistoryStore cache + lazy startup audit.
|
|
3
|
+
*
|
|
4
|
+
* Module-scoped so M2.09's wire handlers can reuse the same store instances
|
|
5
|
+
* the M2.06 lifecycle hook writes to. Audit fires once per project on first
|
|
6
|
+
* access, comparing `_index.jsonl` ↔ `data/` files to flag orphans without
|
|
7
|
+
* auto-fixing (M2.03 compaction owns the cleanup pass).
|
|
8
|
+
*/
|
|
9
|
+
import { readdir } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { TASK_HISTORY_DIR } from '../../constants.js';
|
|
12
|
+
import { getProjectDataDir } from '../../utils/path-utils.js';
|
|
13
|
+
import { processLog } from '../../utils/process-logger.js';
|
|
14
|
+
import { FileTaskHistoryStore } from '../storage/file-task-history-store.js';
|
|
15
|
+
const FILENAME_PATTERN = /^tsk-(.+)\.json$/;
|
|
16
|
+
const MAX_LISTED_ORPHANS = 5;
|
|
17
|
+
const stores = new Map();
|
|
18
|
+
const auditedProjects = new Set();
|
|
19
|
+
/**
|
|
20
|
+
* Daemon boot wall-clock timestamp. Captured at module load so EVERY per-project
|
|
21
|
+
* store shares the same boot reference. The C0 daemon-startup gate inside
|
|
22
|
+
* `FileTaskHistoryStore.isStaleAndRecoverable` uses this to skip stale-recovery
|
|
23
|
+
* for entries written post-boot — those belong to live in-memory tasks whose
|
|
24
|
+
* lifecycle hooks are still firing throttled saves and must not be tombstoned
|
|
25
|
+
* to `INTERRUPTED`.
|
|
26
|
+
*
|
|
27
|
+
* `resetTaskHistoryStoreCache()` re-captures it so tests see fresh boot
|
|
28
|
+
* semantics per `beforeEach`.
|
|
29
|
+
*/
|
|
30
|
+
let daemonStartedAt = Date.now();
|
|
31
|
+
/** Optional logger override for tests — when set, audit triggered inside getStore uses this. */
|
|
32
|
+
let testLoggerForGetStore;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve (or lazily create) the per-project store. The first call for a
|
|
35
|
+
* given `projectPath` schedules a best-effort audit; subsequent calls reuse
|
|
36
|
+
* the cached store and skip re-auditing.
|
|
37
|
+
*/
|
|
38
|
+
export function getStore(projectPath) {
|
|
39
|
+
let store = stores.get(projectPath);
|
|
40
|
+
if (!store) {
|
|
41
|
+
store = new FileTaskHistoryStore({ baseDir: getProjectDataDir(projectPath), daemonStartedAt });
|
|
42
|
+
stores.set(projectPath, store);
|
|
43
|
+
}
|
|
44
|
+
if (!auditedProjects.has(projectPath)) {
|
|
45
|
+
auditedProjects.add(projectPath);
|
|
46
|
+
auditTaskHistory(projectPath, store, testLoggerForGetStore).catch((error) => {
|
|
47
|
+
processLog(`[task-history] audit failed for ${projectPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return store;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Compare `_index.jsonl` (live entries) against the `data/` directory and log
|
|
54
|
+
* orphans. Best-effort: never throws to caller. The `log` parameter defaults
|
|
55
|
+
* to `processLog` for production; tests inject a stub.
|
|
56
|
+
*/
|
|
57
|
+
export async function auditTaskHistory(projectPath, store, log = undefined) {
|
|
58
|
+
const effectiveLog = log ?? processLog;
|
|
59
|
+
const liveEntries = await store.list();
|
|
60
|
+
const liveTaskIds = new Set(liveEntries.map((e) => e.taskId));
|
|
61
|
+
const dataDir = join(getProjectDataDir(projectPath), TASK_HISTORY_DIR, 'data');
|
|
62
|
+
let dataFiles;
|
|
63
|
+
try {
|
|
64
|
+
dataFiles = await readdir(dataDir);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
dataFiles = [];
|
|
68
|
+
}
|
|
69
|
+
const dataTaskIds = new Set();
|
|
70
|
+
for (const filename of dataFiles) {
|
|
71
|
+
const match = FILENAME_PATTERN.exec(filename);
|
|
72
|
+
if (match)
|
|
73
|
+
dataTaskIds.add(match[1]);
|
|
74
|
+
}
|
|
75
|
+
const orphanIndex = [...liveTaskIds].filter((id) => !dataTaskIds.has(id));
|
|
76
|
+
const orphanData = [...dataTaskIds].filter((id) => !liveTaskIds.has(id));
|
|
77
|
+
const head = `[task-history] audit ${projectPath} — ${liveTaskIds.size} live entries, ${dataTaskIds.size} data files.`;
|
|
78
|
+
if (orphanIndex.length === 0 && orphanData.length === 0) {
|
|
79
|
+
effectiveLog(`${head} ok.`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const parts = [];
|
|
83
|
+
if (orphanIndex.length > 0)
|
|
84
|
+
parts.push(formatOrphans('orphan-index', orphanIndex));
|
|
85
|
+
if (orphanData.length > 0)
|
|
86
|
+
parts.push(formatOrphans('orphan-data', orphanData));
|
|
87
|
+
effectiveLog(`${head} WARN: ${parts.join('; ')}.`);
|
|
88
|
+
}
|
|
89
|
+
/** Test-only: clear module-scope state so each test sees a fresh cache. */
|
|
90
|
+
export function resetTaskHistoryStoreCache() {
|
|
91
|
+
stores.clear();
|
|
92
|
+
auditedProjects.clear();
|
|
93
|
+
testLoggerForGetStore = undefined;
|
|
94
|
+
// Re-capture boot time so tests see fresh "this daemon just started" semantics
|
|
95
|
+
// for the C0 stale-recovery gate.
|
|
96
|
+
daemonStartedAt = Date.now();
|
|
97
|
+
}
|
|
98
|
+
/** Test-only: inject a logger into the audit path triggered by `getStore`. Pass no arg to clear. */
|
|
99
|
+
export function _setTestLoggerForGetStore(log) {
|
|
100
|
+
testLoggerForGetStore = log;
|
|
101
|
+
}
|
|
102
|
+
function formatOrphans(label, ids) {
|
|
103
|
+
const listed = ids.slice(0, MAX_LISTED_ORPHANS).map((id) => `tsk-${id}`).join(', ');
|
|
104
|
+
const remainder = ids.length > MAX_LISTED_ORPHANS ? ` (+${ids.length - MAX_LISTED_ORPHANS} more)` : '';
|
|
105
|
+
return `${ids.length} ${label} ${listed}${remainder}`;
|
|
106
|
+
}
|
|
@@ -19,6 +19,7 @@ import type { IAgentPool } from '../../core/interfaces/agent/i-agent-pool.js';
|
|
|
19
19
|
import type { ITaskLifecycleHook } from '../../core/interfaces/process/i-task-lifecycle-hook.js';
|
|
20
20
|
import type { IProjectRegistry } from '../../core/interfaces/project/i-project-registry.js';
|
|
21
21
|
import type { IProjectRouter } from '../../core/interfaces/routing/i-project-router.js';
|
|
22
|
+
import type { ITaskHistoryStore } from '../../core/interfaces/storage/i-task-history-store.js';
|
|
22
23
|
import type { ITransportServer } from '../../core/interfaces/transport/i-transport-server.js';
|
|
23
24
|
import type { TaskInfo } from './types.js';
|
|
24
25
|
/**
|
|
@@ -47,6 +48,13 @@ type TaskRouterOptions = {
|
|
|
47
48
|
agentPool?: IAgentPool;
|
|
48
49
|
/** Function to resolve agent clientId for a given project */
|
|
49
50
|
getAgentForProject: (projectPath?: string) => string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Per-project `ITaskHistoryStore` factory (DIP). When omitted, the new
|
|
53
|
+
* persistent-history handlers (`task:list` paginated, `task:get`,
|
|
54
|
+
* `task:delete*`, `task:clearCompleted`) gracefully degrade to in-memory
|
|
55
|
+
* only — keeping pre-M2.09 unit tests unaffected.
|
|
56
|
+
*/
|
|
57
|
+
getTaskHistoryStore?: (projectPath: string) => ITaskHistoryStore;
|
|
50
58
|
/** Resolves project's review-disabled flag at task-create. Optional; missing → undefined → enabled. */
|
|
51
59
|
isReviewDisabled?: IsReviewDisabledResolver;
|
|
52
60
|
/** Lifecycle hooks for task events (e.g. CurateLogHandler). */
|
|
@@ -61,23 +69,43 @@ type TaskRouterOptions = {
|
|
|
61
69
|
preDispatchCheck?: PreDispatchCheck;
|
|
62
70
|
projectRegistry?: IProjectRegistry;
|
|
63
71
|
projectRouter?: IProjectRouter;
|
|
72
|
+
/**
|
|
73
|
+
* Resolves the active provider/model snapshot at task-create time.
|
|
74
|
+
* Failures are swallowed (fail-open) so dispatch is never blocked.
|
|
75
|
+
*/
|
|
76
|
+
resolveActiveProvider?: () => Promise<{
|
|
77
|
+
model?: string;
|
|
78
|
+
provider?: string;
|
|
79
|
+
}>;
|
|
64
80
|
/** Resolves the projectPath a client registered with (from client:register). */
|
|
65
81
|
resolveClientProjectPath?: (clientId: string) => string | undefined;
|
|
66
82
|
transport: ITransportServer;
|
|
67
83
|
};
|
|
68
84
|
export declare class TaskRouter {
|
|
85
|
+
/**
|
|
86
|
+
* Throttle window for `onTaskUpdate` flushes — bursts of llmservice events
|
|
87
|
+
* are coalesced into one save per window. 100ms keeps perceived latency low
|
|
88
|
+
* while bounding write volume on chatty multi-step agents.
|
|
89
|
+
*/
|
|
90
|
+
private static readonly FLUSH_INTERVAL_MS;
|
|
69
91
|
private readonly agentPool;
|
|
70
92
|
/**
|
|
71
93
|
* Track recently completed tasks for grace period.
|
|
72
94
|
* Allows late-arriving llmservice:* events to be routed even after task:completed.
|
|
73
95
|
*/
|
|
74
96
|
private completedTasks;
|
|
97
|
+
/** TaskIds with pending in-flight mutations awaiting the next throttled flush. */
|
|
98
|
+
private readonly dirtyTaskIds;
|
|
99
|
+
/** Pending throttle timer, if any. */
|
|
100
|
+
private flushTimer;
|
|
75
101
|
private readonly getAgentForProject;
|
|
102
|
+
private readonly getTaskHistoryStore;
|
|
76
103
|
private readonly isReviewDisabled;
|
|
77
104
|
private readonly lifecycleHooks;
|
|
78
105
|
private readonly preDispatchCheck;
|
|
79
106
|
private readonly projectRegistry;
|
|
80
107
|
private readonly projectRouter;
|
|
108
|
+
private readonly resolveActiveProvider;
|
|
81
109
|
private readonly resolveClientProjectPath;
|
|
82
110
|
/** Track active tasks */
|
|
83
111
|
private tasks;
|
|
@@ -117,8 +145,37 @@ export declare class TaskRouter {
|
|
|
117
145
|
* Register all task and LLM event handlers on the transport.
|
|
118
146
|
*/
|
|
119
147
|
setup(): void;
|
|
148
|
+
/**
|
|
149
|
+
* Mutate the live `TaskInfo` from an `llmservice:*` event so a tab refresh
|
|
150
|
+
* during the throttle window sees the in-flight state. Each branch:
|
|
151
|
+
* - thinking: push a `{isThinking: true, content: ''}` marker
|
|
152
|
+
* - chunk(reasoning): append to last item / flip empty marker / push fresh
|
|
153
|
+
* - chunk(text): IGNORED for persistence (transient stream)
|
|
154
|
+
* - response: set responseContent + sessionId (overwrite — multi-step keeps latest)
|
|
155
|
+
* - toolCall: push running entry
|
|
156
|
+
* - toolResult: update existing entry by callId
|
|
157
|
+
* - error / unsupportedInput: IGNORED (terminal hooks capture failure)
|
|
158
|
+
*
|
|
159
|
+
* Mutations use immutable `tasks.set(id, {...task, ...delta})` so consumers
|
|
160
|
+
* holding a captured reference (e.g. notifyHooks*) see a stable snapshot.
|
|
161
|
+
*/
|
|
162
|
+
private accumulateLlmEvent;
|
|
163
|
+
/**
|
|
164
|
+
* Emit `task:deleted` to the project room when a task is removed. Skips
|
|
165
|
+
* silently when no projectPath is resolvable (broadcast wouldn't reach
|
|
166
|
+
* any room). Clients that miss the broadcast will simply not see a row
|
|
167
|
+
* disappear; they reconcile on next `task:list`.
|
|
168
|
+
*/
|
|
169
|
+
private broadcastTaskDeleted;
|
|
170
|
+
/**
|
|
171
|
+
* Drain the dirty set: for each taskId still active, fire `onTaskUpdate` on
|
|
172
|
+
* each lifecycle hook. Tasks moved to `completedTasks` between markDirty
|
|
173
|
+
* and flush are skipped — their terminal lifecycle hook already saved.
|
|
174
|
+
*/
|
|
175
|
+
private flushDirty;
|
|
120
176
|
private handleTaskCancel;
|
|
121
177
|
private handleTaskCancelled;
|
|
178
|
+
private handleTaskClearCompleted;
|
|
122
179
|
private handleTaskCompleted;
|
|
123
180
|
/**
|
|
124
181
|
* Handle task creation from a client.
|
|
@@ -134,7 +191,10 @@ export declare class TaskRouter {
|
|
|
134
191
|
* 6. Submit to agentPool (fire-and-forget)
|
|
135
192
|
*/
|
|
136
193
|
private handleTaskCreate;
|
|
194
|
+
private handleTaskDelete;
|
|
195
|
+
private handleTaskDeleteBulk;
|
|
137
196
|
private handleTaskError;
|
|
197
|
+
private handleTaskGet;
|
|
138
198
|
private handleTaskList;
|
|
139
199
|
/**
|
|
140
200
|
* Emit `task:completed` for a task that the daemon's pre-dispatch gate skipped
|
|
@@ -153,6 +213,12 @@ export declare class TaskRouter {
|
|
|
153
213
|
*/
|
|
154
214
|
private handleTaskSkippedByPreCheck;
|
|
155
215
|
private handleTaskStarted;
|
|
216
|
+
/**
|
|
217
|
+
* Mark a taskId for the next throttled `onTaskUpdate` flush.
|
|
218
|
+
* Schedules a timer if none is pending. Bursts of dirty marks within the
|
|
219
|
+
* 100ms window coalesce into a single flush.
|
|
220
|
+
*/
|
|
221
|
+
private markDirty;
|
|
156
222
|
/**
|
|
157
223
|
* Move a task to the completed tasks map with grace period cleanup.
|
|
158
224
|
*/
|
|
@@ -188,6 +254,12 @@ export declare class TaskRouter {
|
|
|
188
254
|
* Each hook is called independently; errors are caught and logged.
|
|
189
255
|
*/
|
|
190
256
|
private runCreateHooks;
|
|
257
|
+
/**
|
|
258
|
+
* Invoke `resolveActiveProvider` with a try/catch so a thrown resolver
|
|
259
|
+
* cannot block task dispatch. Returns `{}` when no resolver is configured
|
|
260
|
+
* or when the resolver rejects/throws.
|
|
261
|
+
*/
|
|
262
|
+
private safeResolveActiveProvider;
|
|
191
263
|
/**
|
|
192
264
|
* Reads the project's reviewDisabled flag at task-create.
|
|
193
265
|
*
|