@kodrunhq/opencode-autopilot 1.17.0 → 1.19.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/README.md +95 -13
- package/assets/commands/oc-doctor.md +17 -0
- package/assets/commands/oc-update-docs.md +1 -1
- package/bin/configure-tui.ts +1 -1
- package/package.json +1 -1
- package/src/agents/index.ts +0 -12
- package/src/agents/pipeline/index.ts +0 -4
- package/src/autonomy/completion.ts +52 -0
- package/src/autonomy/controller.ts +144 -0
- package/src/autonomy/index.ts +25 -0
- package/src/autonomy/injector.ts +49 -0
- package/src/autonomy/state.ts +91 -0
- package/src/autonomy/types.ts +30 -0
- package/src/autonomy/verification.ts +86 -0
- package/src/background/database.ts +170 -0
- package/src/background/executor.ts +174 -0
- package/src/background/index.ts +8 -0
- package/src/background/manager.ts +232 -0
- package/src/background/repository.ts +174 -0
- package/src/background/schema.ts +24 -0
- package/src/background/sdk-runner.ts +40 -0
- package/src/background/slot-manager.ts +41 -0
- package/src/background/state-machine.ts +19 -0
- package/src/config/v7.ts +3 -3
- package/src/config.ts +105 -21
- package/src/context/budget.ts +45 -0
- package/src/context/compaction-handler.ts +58 -0
- package/src/context/discovery.ts +94 -0
- package/src/context/index.ts +14 -0
- package/src/context/injector.ts +119 -0
- package/src/context/types.ts +24 -0
- package/src/health/checks.ts +214 -3
- package/src/health/index.ts +7 -1
- package/src/health/runner.ts +14 -2
- package/src/index.ts +113 -6
- package/src/installer.ts +13 -0
- package/src/kernel/index.ts +6 -0
- package/src/kernel/migrations.ts +50 -0
- package/src/kernel/retry.ts +49 -0
- package/src/kernel/schema.ts +9 -1
- package/src/kernel/transaction.ts +40 -12
- package/src/logging/forensic-writer.ts +6 -2
- package/src/logging/index.ts +2 -0
- package/src/mcp/index.ts +34 -0
- package/src/mcp/manager.ts +206 -0
- package/src/mcp/scope-filter.ts +44 -0
- package/src/mcp/types.ts +38 -0
- package/src/orchestrator/arena.ts +7 -1
- package/src/orchestrator/fallback/event-handler.ts +12 -1
- package/src/orchestrator/handlers/challenge.ts +8 -1
- package/src/orchestrator/handlers/plan.ts +8 -1
- package/src/orchestrator/handlers/recon.ts +8 -1
- package/src/orchestrator/handlers/types.ts +2 -2
- package/src/orchestrator/lesson-memory.ts +6 -1
- package/src/orchestrator/orchestration-logger.ts +15 -3
- package/src/orchestrator/skill-injection.ts +7 -1
- package/src/orchestrator/state.ts +6 -1
- package/src/recovery/classifier.ts +127 -0
- package/src/recovery/event-handler.ts +263 -0
- package/src/recovery/index.ts +20 -0
- package/src/recovery/orchestrator.ts +180 -0
- package/src/recovery/persistence.ts +87 -0
- package/src/recovery/strategies.ts +107 -0
- package/src/recovery/types.ts +31 -0
- package/src/registry/model-groups.ts +2 -19
- package/src/registry/resolver.ts +38 -9
- package/src/review/agent-catalog.ts +83 -251
- package/src/review/agents/architecture-verifier.ts +41 -0
- package/src/review/agents/code-hygiene-auditor.ts +40 -0
- package/src/review/agents/correctness-auditor.ts +41 -0
- package/src/review/agents/frontend-auditor.ts +39 -0
- package/src/review/agents/index.ts +15 -42
- package/src/review/agents/language-idioms-auditor.ts +39 -0
- package/src/review/agents/security-auditor.ts +12 -8
- package/src/review/stack-gate.ts +2 -6
- package/src/routing/categories.ts +111 -0
- package/src/routing/classifier.ts +152 -0
- package/src/routing/engine.ts +89 -0
- package/src/routing/index.ts +4 -0
- package/src/routing/types.ts +14 -0
- package/src/skills/adaptive-injector.ts +34 -3
- package/src/skills/loader.ts +4 -0
- package/src/tools/background.ts +196 -0
- package/src/tools/configure.ts +1 -1
- package/src/tools/delegate.ts +205 -0
- package/src/tools/loop.ts +94 -0
- package/src/tools/recover.ts +172 -0
- package/src/types/background.ts +51 -0
- package/src/types/mcp.ts +27 -0
- package/src/types/recovery.ts +49 -0
- package/src/types/routing.ts +39 -0
- package/src/ux/context-warnings.ts +81 -0
- package/src/ux/error-hints.ts +38 -0
- package/src/ux/index.ts +7 -0
- package/src/ux/notifications.ts +67 -0
- package/src/ux/progress.ts +77 -0
- package/src/ux/session-summary.ts +67 -0
- package/src/ux/task-status.ts +109 -0
- package/src/ux/types.ts +24 -0
- package/src/agents/db-specialist.ts +0 -295
- package/src/agents/devops.ts +0 -352
- package/src/agents/documenter.ts +0 -44
- package/src/agents/frontend-engineer.ts +0 -541
- package/src/agents/pipeline/oc-explorer.ts +0 -46
- package/src/agents/pipeline/oc-retrospector.ts +0 -42
- package/src/review/agents/auth-flow-verifier.ts +0 -47
- package/src/review/agents/concurrency-checker.ts +0 -47
- package/src/review/agents/dead-code-scanner.ts +0 -47
- package/src/review/agents/go-idioms-auditor.ts +0 -46
- package/src/review/agents/python-django-auditor.ts +0 -46
- package/src/review/agents/react-patterns-auditor.ts +0 -46
- package/src/review/agents/rust-safety-auditor.ts +0 -46
- package/src/review/agents/scope-intent-verifier.ts +0 -45
- package/src/review/agents/silent-failure-hunter.ts +0 -45
- package/src/review/agents/spec-checker.ts +0 -45
- package/src/review/agents/state-mgmt-auditor.ts +0 -46
- package/src/review/agents/type-soundness.ts +0 -46
- package/src/review/agents/wiring-inspector.ts +0 -46
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { withTransaction } from "../kernel/transaction";
|
|
3
|
+
import type { TaskStatus } from "../types/background";
|
|
4
|
+
import {
|
|
5
|
+
type BackgroundTaskRecord,
|
|
6
|
+
type BackgroundTaskResultRecord,
|
|
7
|
+
type CreateBackgroundTaskInput,
|
|
8
|
+
createTaskId,
|
|
9
|
+
createTimestamp,
|
|
10
|
+
getTaskByIdRow,
|
|
11
|
+
listBackgroundTasks,
|
|
12
|
+
} from "./database";
|
|
13
|
+
import { assertTransition } from "./state-machine";
|
|
14
|
+
|
|
15
|
+
export interface TaskUpdatePayload {
|
|
16
|
+
readonly result?: string | null;
|
|
17
|
+
readonly error?: string | null;
|
|
18
|
+
readonly now?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createTask(db: Database, task: CreateBackgroundTaskInput): BackgroundTaskRecord {
|
|
22
|
+
return withTransaction(db, () => {
|
|
23
|
+
const timestamp = task.createdAt ?? createTimestamp();
|
|
24
|
+
const nextTask = Object.freeze({
|
|
25
|
+
id: task.id ?? createTaskId(),
|
|
26
|
+
sessionId: task.sessionId,
|
|
27
|
+
description: task.description,
|
|
28
|
+
category: task.category ?? null,
|
|
29
|
+
status: task.status ?? "pending",
|
|
30
|
+
result: task.result ?? null,
|
|
31
|
+
error: task.error ?? null,
|
|
32
|
+
agent: task.agent ?? null,
|
|
33
|
+
model: task.model ?? null,
|
|
34
|
+
priority: task.priority ?? 50,
|
|
35
|
+
createdAt: timestamp,
|
|
36
|
+
updatedAt: task.updatedAt ?? timestamp,
|
|
37
|
+
startedAt: task.startedAt ?? null,
|
|
38
|
+
completedAt: task.completedAt ?? null,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
db.run(
|
|
42
|
+
`INSERT INTO background_tasks (
|
|
43
|
+
id, session_id, description, category, status, result, error, agent, model, priority,
|
|
44
|
+
created_at, updated_at, started_at, completed_at
|
|
45
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
46
|
+
[
|
|
47
|
+
nextTask.id,
|
|
48
|
+
nextTask.sessionId,
|
|
49
|
+
nextTask.description,
|
|
50
|
+
nextTask.category,
|
|
51
|
+
nextTask.status,
|
|
52
|
+
nextTask.result,
|
|
53
|
+
nextTask.error,
|
|
54
|
+
nextTask.agent,
|
|
55
|
+
nextTask.model,
|
|
56
|
+
nextTask.priority,
|
|
57
|
+
nextTask.createdAt,
|
|
58
|
+
nextTask.updatedAt,
|
|
59
|
+
nextTask.startedAt,
|
|
60
|
+
nextTask.completedAt,
|
|
61
|
+
],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const createdTask = getTaskByIdRow(db, nextTask.id);
|
|
65
|
+
if (!createdTask) {
|
|
66
|
+
throw new Error(`Failed to create background task '${nextTask.id}'`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return createdTask;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function updateStatus(
|
|
74
|
+
db: Database,
|
|
75
|
+
taskId: string,
|
|
76
|
+
newStatus: TaskStatus,
|
|
77
|
+
payload: TaskUpdatePayload = {},
|
|
78
|
+
): BackgroundTaskRecord {
|
|
79
|
+
return withTransaction(db, () => {
|
|
80
|
+
const currentTask = getTaskByIdRow(db, taskId);
|
|
81
|
+
if (!currentTask) {
|
|
82
|
+
throw new Error(`Background task '${taskId}' not found`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
assertTransition(currentTask.status, newStatus);
|
|
86
|
+
|
|
87
|
+
const timestamp = payload.now ?? createTimestamp();
|
|
88
|
+
const startedAt =
|
|
89
|
+
newStatus === "running" ? (currentTask.startedAt ?? timestamp) : currentTask.startedAt;
|
|
90
|
+
const completedAt =
|
|
91
|
+
newStatus === "completed" || newStatus === "failed" || newStatus === "cancelled"
|
|
92
|
+
? timestamp
|
|
93
|
+
: currentTask.completedAt;
|
|
94
|
+
const result =
|
|
95
|
+
newStatus === "completed" ? (payload.result ?? currentTask.result) : currentTask.result;
|
|
96
|
+
const error = newStatus === "failed" ? (payload.error ?? currentTask.error) : null;
|
|
97
|
+
|
|
98
|
+
db.run(
|
|
99
|
+
`UPDATE background_tasks
|
|
100
|
+
SET status = ?, result = ?, error = ?, updated_at = ?, started_at = ?, completed_at = ?
|
|
101
|
+
WHERE id = ?`,
|
|
102
|
+
[newStatus, result, error, timestamp, startedAt, completedAt, taskId],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const updatedTask = getTaskByIdRow(db, taskId);
|
|
106
|
+
if (!updatedTask) {
|
|
107
|
+
throw new Error(`Failed to update background task '${taskId}'`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return updatedTask;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getActiveTasks(db: Database, sessionId?: string): readonly BackgroundTaskRecord[] {
|
|
115
|
+
return listBackgroundTasks(db, {
|
|
116
|
+
sessionId,
|
|
117
|
+
statuses: ["pending", "running"],
|
|
118
|
+
prioritizePending: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getTaskById(db: Database, taskId: string): BackgroundTaskRecord | null {
|
|
123
|
+
return getTaskByIdRow(db, taskId);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getTaskResult(db: Database, taskId: string): BackgroundTaskResultRecord | null {
|
|
127
|
+
const task = getTaskByIdRow(db, taskId);
|
|
128
|
+
if (!task) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return Object.freeze({
|
|
133
|
+
status: task.status,
|
|
134
|
+
result: task.result,
|
|
135
|
+
error: task.error,
|
|
136
|
+
completedAt: task.completedAt,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function cancelTask(db: Database, taskId: string): BackgroundTaskRecord | null {
|
|
141
|
+
const task = getTaskByIdRow(db, taskId);
|
|
142
|
+
if (!task) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (task.status !== "pending" && task.status !== "running") {
|
|
147
|
+
return task;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return updateStatus(db, taskId, "cancelled");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function countByStatus(db: Database, status: TaskStatus): number {
|
|
154
|
+
const row = db
|
|
155
|
+
.query("SELECT COUNT(*) as count FROM background_tasks WHERE status = ?")
|
|
156
|
+
.get(status) as { count?: number } | null;
|
|
157
|
+
return row?.count ?? 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function enforceMaxConcurrent(db: Database, maxConcurrent: number): void {
|
|
161
|
+
const runningCount = countByStatus(db, "running");
|
|
162
|
+
if (runningCount >= maxConcurrent) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Background concurrency limit reached: ${runningCount} running task(s), max ${maxConcurrent}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function listTasks(
|
|
170
|
+
db: Database,
|
|
171
|
+
filters: { readonly sessionId?: string; readonly status?: TaskStatus } = {},
|
|
172
|
+
): readonly BackgroundTaskRecord[] {
|
|
173
|
+
return listBackgroundTasks(db, filters);
|
|
174
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const BACKGROUND_TASKS_TABLE_STATEMENT = `CREATE TABLE IF NOT EXISTS background_tasks (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
session_id TEXT NOT NULL,
|
|
4
|
+
description TEXT NOT NULL,
|
|
5
|
+
category TEXT,
|
|
6
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
7
|
+
result TEXT,
|
|
8
|
+
error TEXT,
|
|
9
|
+
agent TEXT,
|
|
10
|
+
model TEXT,
|
|
11
|
+
priority INTEGER NOT NULL DEFAULT 50,
|
|
12
|
+
created_at TEXT NOT NULL,
|
|
13
|
+
updated_at TEXT NOT NULL,
|
|
14
|
+
started_at TEXT,
|
|
15
|
+
completed_at TEXT
|
|
16
|
+
)`;
|
|
17
|
+
|
|
18
|
+
export const BACKGROUND_TASKS_INDEX_STATEMENT =
|
|
19
|
+
"CREATE INDEX IF NOT EXISTS idx_background_tasks_status_created_at ON background_tasks(status, created_at)";
|
|
20
|
+
|
|
21
|
+
export const BACKGROUND_TASKS_SCHEMA_STATEMENTS: readonly string[] = Object.freeze([
|
|
22
|
+
BACKGROUND_TASKS_TABLE_STATEMENT,
|
|
23
|
+
BACKGROUND_TASKS_INDEX_STATEMENT,
|
|
24
|
+
]);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getLogger } from "../logging/domains";
|
|
2
|
+
import type { BackgroundTaskRecord } from "./database";
|
|
3
|
+
|
|
4
|
+
export interface BackgroundSdkOperations {
|
|
5
|
+
readonly promptAsync: (
|
|
6
|
+
sessionId: string,
|
|
7
|
+
model: string | undefined,
|
|
8
|
+
parts: ReadonlyArray<{ type: "text"; text: string }>,
|
|
9
|
+
) => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const logger = getLogger("background", "sdk-runner");
|
|
13
|
+
|
|
14
|
+
export function createSdkRunner(
|
|
15
|
+
sdk: BackgroundSdkOperations,
|
|
16
|
+
): (task: BackgroundTaskRecord, signal: AbortSignal) => Promise<string> {
|
|
17
|
+
return async (task, signal) => {
|
|
18
|
+
if (signal.aborted) {
|
|
19
|
+
throw signal.reason ?? new Error("Aborted");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const model = task.model ?? undefined;
|
|
23
|
+
const parts: ReadonlyArray<{ type: "text"; text: string }> = [
|
|
24
|
+
{ type: "text", text: task.description },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
logger.info("Dispatching background task via SDK", {
|
|
28
|
+
backgroundTaskId: task.id,
|
|
29
|
+
sessionId: task.sessionId,
|
|
30
|
+
agent: task.agent,
|
|
31
|
+
model: task.model,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await sdk.promptAsync(task.sessionId, model, parts);
|
|
35
|
+
|
|
36
|
+
const agentLabel = task.agent ? ` via ${task.agent}` : "";
|
|
37
|
+
const modelLabel = task.model ? ` (${task.model})` : "";
|
|
38
|
+
return `Dispatched${agentLabel}${modelLabel}: ${task.description}`;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class SlotManager {
|
|
2
|
+
private readonly occupiedSlots = new Set<string>();
|
|
3
|
+
|
|
4
|
+
constructor(private readonly maxSlots: number) {
|
|
5
|
+
if (!Number.isInteger(maxSlots) || maxSlots < 1) {
|
|
6
|
+
throw new Error("SlotManager requires at least one slot");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
acquire(): string | null {
|
|
11
|
+
if (this.isFull()) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
for (let index = 1; index <= this.maxSlots; index += 1) {
|
|
16
|
+
const slotId = `slot-${index}`;
|
|
17
|
+
if (!this.occupiedSlots.has(slotId)) {
|
|
18
|
+
this.occupiedSlots.add(slotId);
|
|
19
|
+
return slotId;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
release(slotId: string): void {
|
|
27
|
+
this.occupiedSlots.delete(slotId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getActiveCount(): number {
|
|
31
|
+
return this.occupiedSlots.size;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
isFull(): boolean {
|
|
35
|
+
return this.occupiedSlots.size >= this.maxSlots;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getCapacity(): number {
|
|
39
|
+
return this.maxSlots;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TaskStatus } from "../types/background";
|
|
2
|
+
|
|
3
|
+
const VALID_TRANSITIONS: Readonly<Record<TaskStatus, readonly TaskStatus[]>> = Object.freeze({
|
|
4
|
+
pending: Object.freeze<TaskStatus[]>(["running", "cancelled"]),
|
|
5
|
+
running: Object.freeze<TaskStatus[]>(["completed", "failed", "cancelled"]),
|
|
6
|
+
completed: Object.freeze<TaskStatus[]>([]),
|
|
7
|
+
failed: Object.freeze<TaskStatus[]>([]),
|
|
8
|
+
cancelled: Object.freeze<TaskStatus[]>([]),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function validateTransition(from: TaskStatus, to: TaskStatus): boolean {
|
|
12
|
+
return VALID_TRANSITIONS[from].includes(to);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assertTransition(from: TaskStatus, to: TaskStatus): void {
|
|
16
|
+
if (!validateTransition(from, to)) {
|
|
17
|
+
throw new Error(`Invalid background task transition from '${from}' to '${to}'`);
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/config/v7.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type PluginConfigV7 = Omit<PluginConfig, "version"> & {
|
|
|
5
5
|
readonly background?: {
|
|
6
6
|
readonly enabled: boolean;
|
|
7
7
|
readonly maxConcurrent: number;
|
|
8
|
-
readonly
|
|
8
|
+
readonly persistence: boolean;
|
|
9
9
|
};
|
|
10
10
|
readonly autonomy?: {
|
|
11
11
|
readonly enabled: boolean;
|
|
@@ -21,7 +21,7 @@ export function migrateV6toV7(v6Config: PluginConfig): PluginConfigV7 {
|
|
|
21
21
|
background: {
|
|
22
22
|
enabled: true,
|
|
23
23
|
maxConcurrent: 5,
|
|
24
|
-
|
|
24
|
+
persistence: true,
|
|
25
25
|
},
|
|
26
26
|
autonomy: {
|
|
27
27
|
enabled: false,
|
|
@@ -35,7 +35,7 @@ export const v7ConfigDefaults = {
|
|
|
35
35
|
background: {
|
|
36
36
|
enabled: true,
|
|
37
37
|
maxConcurrent: 5,
|
|
38
|
-
|
|
38
|
+
persistence: true,
|
|
39
39
|
},
|
|
40
40
|
autonomy: {
|
|
41
41
|
enabled: false,
|
package/src/config.ts
CHANGED
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
testModeDefaults,
|
|
11
11
|
} from "./orchestrator/fallback/fallback-config";
|
|
12
12
|
import { AGENT_REGISTRY, ALL_GROUP_IDS } from "./registry/model-groups";
|
|
13
|
+
import { backgroundConfigSchema, backgroundDefaults } from "./types/background";
|
|
14
|
+
import { mcpConfigSchema, mcpDefaults } from "./types/mcp";
|
|
15
|
+
import { recoveryConfigSchema, recoveryDefaults } from "./types/recovery";
|
|
16
|
+
import { routingConfigSchema, routingDefaults } from "./types/routing";
|
|
13
17
|
import { ensureDir, isEnoentError } from "./utils/fs-helpers";
|
|
14
18
|
import { getGlobalConfigDir } from "./utils/paths";
|
|
15
19
|
|
|
@@ -182,14 +186,47 @@ const pluginConfigSchemaV6 = z
|
|
|
182
186
|
}
|
|
183
187
|
});
|
|
184
188
|
|
|
185
|
-
|
|
186
|
-
export const pluginConfigSchema = pluginConfigSchemaV6;
|
|
189
|
+
type PluginConfigV6 = z.infer<typeof pluginConfigSchemaV6>;
|
|
187
190
|
|
|
188
|
-
|
|
191
|
+
const pluginConfigSchemaV7 = z
|
|
192
|
+
.object({
|
|
193
|
+
version: z.literal(7),
|
|
194
|
+
configured: z.boolean(),
|
|
195
|
+
groups: z.record(z.string(), groupModelAssignmentSchema).default({}),
|
|
196
|
+
overrides: z.record(z.string(), agentOverrideSchema).default({}),
|
|
197
|
+
orchestrator: orchestratorConfigSchema.default(orchestratorDefaults),
|
|
198
|
+
confidence: confidenceConfigSchema.default(confidenceDefaults),
|
|
199
|
+
fallback: fallbackConfigSchemaV6.default(fallbackDefaultsV6),
|
|
200
|
+
memory: memoryConfigSchema.default(memoryDefaults),
|
|
201
|
+
background: backgroundConfigSchema.default(backgroundDefaults),
|
|
202
|
+
autonomy: z
|
|
203
|
+
.object({
|
|
204
|
+
enabled: z.boolean().default(false),
|
|
205
|
+
verification: z.enum(["strict", "normal", "lenient"]).default("normal"),
|
|
206
|
+
maxIterations: z.number().int().min(1).max(50).default(10),
|
|
207
|
+
})
|
|
208
|
+
.default({ enabled: false, verification: "normal", maxIterations: 10 }),
|
|
209
|
+
routing: routingConfigSchema.default(routingDefaults),
|
|
210
|
+
recovery: recoveryConfigSchema.default(recoveryDefaults),
|
|
211
|
+
mcp: mcpConfigSchema.default(mcpDefaults),
|
|
212
|
+
})
|
|
213
|
+
.superRefine((config, ctx) => {
|
|
214
|
+
for (const groupId of Object.keys(config.groups)) {
|
|
215
|
+
if (!ALL_GROUP_IDS.includes(groupId as (typeof ALL_GROUP_IDS)[number])) {
|
|
216
|
+
ctx.addIssue({
|
|
217
|
+
code: z.ZodIssueCode.custom,
|
|
218
|
+
path: ["groups", groupId],
|
|
219
|
+
message: `Unknown group id "${groupId}". Expected one of: ${ALL_GROUP_IDS.join(", ")}`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
189
224
|
|
|
190
|
-
export const
|
|
225
|
+
export const pluginConfigSchema = pluginConfigSchemaV7;
|
|
226
|
+
|
|
227
|
+
export type PluginConfig = z.infer<typeof pluginConfigSchemaV7>;
|
|
191
228
|
|
|
192
|
-
|
|
229
|
+
export const CONFIG_PATH = join(getGlobalConfigDir(), "opencode-autopilot.json");
|
|
193
230
|
|
|
194
231
|
function migrateV1toV2(v1Config: PluginConfigV1): PluginConfigV2 {
|
|
195
232
|
return {
|
|
@@ -274,7 +311,7 @@ function migrateV4toV5(v4Config: PluginConfigV4): PluginConfigV5 {
|
|
|
274
311
|
};
|
|
275
312
|
}
|
|
276
313
|
|
|
277
|
-
function migrateV5toV6(v5Config: PluginConfigV5):
|
|
314
|
+
function migrateV5toV6(v5Config: PluginConfigV5): PluginConfigV6 {
|
|
278
315
|
return {
|
|
279
316
|
version: 6 as const,
|
|
280
317
|
configured: v5Config.configured,
|
|
@@ -287,68 +324,106 @@ function migrateV5toV6(v5Config: PluginConfigV5): PluginConfig {
|
|
|
287
324
|
};
|
|
288
325
|
}
|
|
289
326
|
|
|
290
|
-
|
|
327
|
+
export const v7ConfigDefaults = {
|
|
328
|
+
background: backgroundDefaults,
|
|
329
|
+
autonomy: {
|
|
330
|
+
enabled: false,
|
|
331
|
+
verification: "normal" as const,
|
|
332
|
+
maxIterations: 10,
|
|
333
|
+
},
|
|
334
|
+
routing: routingDefaults,
|
|
335
|
+
recovery: recoveryDefaults,
|
|
336
|
+
mcp: mcpDefaults,
|
|
337
|
+
} as const;
|
|
338
|
+
|
|
339
|
+
export function migrateV6toV7(v6Config: PluginConfigV6): PluginConfig {
|
|
340
|
+
return {
|
|
341
|
+
version: 7 as const,
|
|
342
|
+
configured: v6Config.configured,
|
|
343
|
+
groups: v6Config.groups,
|
|
344
|
+
overrides: v6Config.overrides,
|
|
345
|
+
orchestrator: v6Config.orchestrator,
|
|
346
|
+
confidence: v6Config.confidence,
|
|
347
|
+
fallback: v6Config.fallback,
|
|
348
|
+
memory: v6Config.memory,
|
|
349
|
+
background: backgroundDefaults,
|
|
350
|
+
autonomy: {
|
|
351
|
+
enabled: false,
|
|
352
|
+
verification: "normal",
|
|
353
|
+
maxIterations: 10,
|
|
354
|
+
},
|
|
355
|
+
routing: routingDefaults,
|
|
356
|
+
recovery: recoveryDefaults,
|
|
357
|
+
mcp: mcpDefaults,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
291
360
|
|
|
292
361
|
export async function loadConfig(configPath: string = CONFIG_PATH): Promise<PluginConfig | null> {
|
|
293
362
|
try {
|
|
294
363
|
const raw = await readFile(configPath, "utf-8");
|
|
295
364
|
const parsed = JSON.parse(raw);
|
|
296
365
|
|
|
297
|
-
|
|
366
|
+
const v7Result = pluginConfigSchemaV7.safeParse(parsed);
|
|
367
|
+
if (v7Result.success) return v7Result.data;
|
|
368
|
+
|
|
298
369
|
const v6Result = pluginConfigSchemaV6.safeParse(parsed);
|
|
299
|
-
if (v6Result.success)
|
|
370
|
+
if (v6Result.success) {
|
|
371
|
+
const migrated = migrateV6toV7(v6Result.data);
|
|
372
|
+
await saveConfig(migrated, configPath);
|
|
373
|
+
return migrated;
|
|
374
|
+
}
|
|
300
375
|
|
|
301
|
-
// Try v5 and migrate to v6
|
|
302
376
|
const v5Result = pluginConfigSchemaV5.safeParse(parsed);
|
|
303
377
|
if (v5Result.success) {
|
|
304
|
-
const
|
|
378
|
+
const v6 = migrateV5toV6(v5Result.data);
|
|
379
|
+
const migrated = migrateV6toV7(v6);
|
|
305
380
|
await saveConfig(migrated, configPath);
|
|
306
381
|
return migrated;
|
|
307
382
|
}
|
|
308
383
|
|
|
309
|
-
// Try v4 → v5 → v6
|
|
310
384
|
const v4Result = pluginConfigSchemaV4.safeParse(parsed);
|
|
311
385
|
if (v4Result.success) {
|
|
312
386
|
const v5 = migrateV4toV5(v4Result.data);
|
|
313
|
-
const
|
|
387
|
+
const v6 = migrateV5toV6(v5);
|
|
388
|
+
const migrated = migrateV6toV7(v6);
|
|
314
389
|
await saveConfig(migrated, configPath);
|
|
315
390
|
return migrated;
|
|
316
391
|
}
|
|
317
392
|
|
|
318
|
-
// Try v3 → v4 → v5 → v6
|
|
319
393
|
const v3Result = pluginConfigSchemaV3.safeParse(parsed);
|
|
320
394
|
if (v3Result.success) {
|
|
321
395
|
const v4 = migrateV3toV4(v3Result.data);
|
|
322
396
|
const v5 = migrateV4toV5(v4);
|
|
323
|
-
const
|
|
397
|
+
const v6 = migrateV5toV6(v5);
|
|
398
|
+
const migrated = migrateV6toV7(v6);
|
|
324
399
|
await saveConfig(migrated, configPath);
|
|
325
400
|
return migrated;
|
|
326
401
|
}
|
|
327
402
|
|
|
328
|
-
// Try v2 → v3 → v4 → v5 → v6
|
|
329
403
|
const v2Result = pluginConfigSchemaV2.safeParse(parsed);
|
|
330
404
|
if (v2Result.success) {
|
|
331
405
|
const v3 = migrateV2toV3(v2Result.data);
|
|
332
406
|
const v4 = migrateV3toV4(v3);
|
|
333
407
|
const v5 = migrateV4toV5(v4);
|
|
334
|
-
const
|
|
408
|
+
const v6 = migrateV5toV6(v5);
|
|
409
|
+
const migrated = migrateV6toV7(v6);
|
|
335
410
|
await saveConfig(migrated, configPath);
|
|
336
411
|
return migrated;
|
|
337
412
|
}
|
|
338
413
|
|
|
339
|
-
// Try v1 → v2 → v3 → v4 → v5 → v6
|
|
340
414
|
const v1Result = pluginConfigSchemaV1.safeParse(parsed);
|
|
341
415
|
if (v1Result.success) {
|
|
342
416
|
const v2 = migrateV1toV2(v1Result.data);
|
|
343
417
|
const v3 = migrateV2toV3(v2);
|
|
344
418
|
const v4 = migrateV3toV4(v3);
|
|
345
419
|
const v5 = migrateV4toV5(v4);
|
|
346
|
-
const
|
|
420
|
+
const v6 = migrateV5toV6(v5);
|
|
421
|
+
const migrated = migrateV6toV7(v6);
|
|
347
422
|
await saveConfig(migrated, configPath);
|
|
348
423
|
return migrated;
|
|
349
424
|
}
|
|
350
425
|
|
|
351
|
-
return
|
|
426
|
+
return pluginConfigSchemaV7.parse(parsed);
|
|
352
427
|
} catch (error: unknown) {
|
|
353
428
|
if (isEnoentError(error)) return null;
|
|
354
429
|
throw error;
|
|
@@ -371,7 +446,7 @@ export function isFirstLoad(config: PluginConfig | null): boolean {
|
|
|
371
446
|
|
|
372
447
|
export function createDefaultConfig(): PluginConfig {
|
|
373
448
|
return {
|
|
374
|
-
version:
|
|
449
|
+
version: 7 as const,
|
|
375
450
|
configured: false,
|
|
376
451
|
groups: {},
|
|
377
452
|
overrides: {},
|
|
@@ -379,5 +454,14 @@ export function createDefaultConfig(): PluginConfig {
|
|
|
379
454
|
confidence: confidenceDefaults,
|
|
380
455
|
fallback: fallbackDefaultsV6,
|
|
381
456
|
memory: memoryDefaults,
|
|
457
|
+
background: backgroundDefaults,
|
|
458
|
+
autonomy: {
|
|
459
|
+
enabled: false,
|
|
460
|
+
verification: "normal",
|
|
461
|
+
maxIterations: 10,
|
|
462
|
+
},
|
|
463
|
+
routing: routingDefaults,
|
|
464
|
+
recovery: recoveryDefaults,
|
|
465
|
+
mcp: mcpDefaults,
|
|
382
466
|
};
|
|
383
467
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ContextSource } from "./types";
|
|
2
|
+
|
|
3
|
+
const CHARS_PER_TOKEN = 4;
|
|
4
|
+
const DEFAULT_TOTAL_BUDGET = 4000;
|
|
5
|
+
const TRUNCATION_SUFFIX = "... [truncated]";
|
|
6
|
+
|
|
7
|
+
export function truncateToTokens(content: string, maxTokens: number): string {
|
|
8
|
+
const maxChars = Math.max(0, Math.floor(maxTokens * CHARS_PER_TOKEN));
|
|
9
|
+
if (content.length <= maxChars) {
|
|
10
|
+
return content;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (maxChars === 0) {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (maxChars <= TRUNCATION_SUFFIX.length) {
|
|
18
|
+
return TRUNCATION_SUFFIX.slice(0, maxChars);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const truncatedContent = content.slice(0, maxChars - TRUNCATION_SUFFIX.length).trimEnd();
|
|
22
|
+
return `${truncatedContent}${TRUNCATION_SUFFIX}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function allocateBudget(
|
|
26
|
+
sources: readonly ContextSource[],
|
|
27
|
+
totalBudget: number = DEFAULT_TOTAL_BUDGET,
|
|
28
|
+
): { readonly allocations: ReadonlyMap<string, number>; readonly totalUsed: number } {
|
|
29
|
+
const orderedSources = [...sources].sort(
|
|
30
|
+
(left, right) => right.priority - left.priority || left.filePath.localeCompare(right.filePath),
|
|
31
|
+
);
|
|
32
|
+
const allocations = new Map<string, number>();
|
|
33
|
+
let remainingBudget = Math.max(0, totalBudget);
|
|
34
|
+
|
|
35
|
+
for (const source of orderedSources) {
|
|
36
|
+
const allocation = Math.min(source.tokenEstimate, remainingBudget);
|
|
37
|
+
allocations.set(source.filePath, allocation);
|
|
38
|
+
remainingBudget -= allocation;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
allocations,
|
|
43
|
+
totalUsed: Math.max(0, totalBudget) - remainingBudget,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getLogger } from "../logging/domains";
|
|
2
|
+
import type { createContextInjector } from "./injector";
|
|
3
|
+
|
|
4
|
+
const logger = getLogger("context", "compaction-handler");
|
|
5
|
+
|
|
6
|
+
interface EventProperties {
|
|
7
|
+
readonly sessionID?: string;
|
|
8
|
+
readonly info?: {
|
|
9
|
+
readonly sessionID?: string;
|
|
10
|
+
readonly id?: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function extractSessionID(properties: unknown): string | undefined {
|
|
15
|
+
if (!properties || typeof properties !== "object") {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const eventProperties = properties as EventProperties;
|
|
20
|
+
if (typeof eventProperties.sessionID === "string") {
|
|
21
|
+
return eventProperties.sessionID;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof eventProperties.info?.sessionID === "string") {
|
|
25
|
+
return eventProperties.info.sessionID;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof eventProperties.info?.id === "string") {
|
|
29
|
+
return eventProperties.info.id;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createCompactionHandler(injector: ReturnType<typeof createContextInjector>) {
|
|
36
|
+
return async (input: {
|
|
37
|
+
readonly event: {
|
|
38
|
+
readonly type: string;
|
|
39
|
+
readonly properties?: unknown;
|
|
40
|
+
};
|
|
41
|
+
}): Promise<void> => {
|
|
42
|
+
try {
|
|
43
|
+
if (input.event.type !== "session.compacted") {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sessionID = extractSessionID(input.event.properties);
|
|
48
|
+
if (!sessionID) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
injector.clearCache(sessionID);
|
|
53
|
+
await injector({ sessionID }, { system: [] });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
logger.warn("context compaction handling failed", { error: String(error) });
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|