@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.
Files changed (118) hide show
  1. package/README.md +95 -13
  2. package/assets/commands/oc-doctor.md +17 -0
  3. package/assets/commands/oc-update-docs.md +1 -1
  4. package/bin/configure-tui.ts +1 -1
  5. package/package.json +1 -1
  6. package/src/agents/index.ts +0 -12
  7. package/src/agents/pipeline/index.ts +0 -4
  8. package/src/autonomy/completion.ts +52 -0
  9. package/src/autonomy/controller.ts +144 -0
  10. package/src/autonomy/index.ts +25 -0
  11. package/src/autonomy/injector.ts +49 -0
  12. package/src/autonomy/state.ts +91 -0
  13. package/src/autonomy/types.ts +30 -0
  14. package/src/autonomy/verification.ts +86 -0
  15. package/src/background/database.ts +170 -0
  16. package/src/background/executor.ts +174 -0
  17. package/src/background/index.ts +8 -0
  18. package/src/background/manager.ts +232 -0
  19. package/src/background/repository.ts +174 -0
  20. package/src/background/schema.ts +24 -0
  21. package/src/background/sdk-runner.ts +40 -0
  22. package/src/background/slot-manager.ts +41 -0
  23. package/src/background/state-machine.ts +19 -0
  24. package/src/config/v7.ts +3 -3
  25. package/src/config.ts +105 -21
  26. package/src/context/budget.ts +45 -0
  27. package/src/context/compaction-handler.ts +58 -0
  28. package/src/context/discovery.ts +94 -0
  29. package/src/context/index.ts +14 -0
  30. package/src/context/injector.ts +119 -0
  31. package/src/context/types.ts +24 -0
  32. package/src/health/checks.ts +214 -3
  33. package/src/health/index.ts +7 -1
  34. package/src/health/runner.ts +14 -2
  35. package/src/index.ts +113 -6
  36. package/src/installer.ts +13 -0
  37. package/src/kernel/index.ts +6 -0
  38. package/src/kernel/migrations.ts +50 -0
  39. package/src/kernel/retry.ts +49 -0
  40. package/src/kernel/schema.ts +9 -1
  41. package/src/kernel/transaction.ts +40 -12
  42. package/src/logging/forensic-writer.ts +6 -2
  43. package/src/logging/index.ts +2 -0
  44. package/src/mcp/index.ts +34 -0
  45. package/src/mcp/manager.ts +206 -0
  46. package/src/mcp/scope-filter.ts +44 -0
  47. package/src/mcp/types.ts +38 -0
  48. package/src/orchestrator/arena.ts +7 -1
  49. package/src/orchestrator/fallback/event-handler.ts +12 -1
  50. package/src/orchestrator/handlers/challenge.ts +8 -1
  51. package/src/orchestrator/handlers/plan.ts +8 -1
  52. package/src/orchestrator/handlers/recon.ts +8 -1
  53. package/src/orchestrator/handlers/types.ts +2 -2
  54. package/src/orchestrator/lesson-memory.ts +6 -1
  55. package/src/orchestrator/orchestration-logger.ts +15 -3
  56. package/src/orchestrator/skill-injection.ts +7 -1
  57. package/src/orchestrator/state.ts +6 -1
  58. package/src/recovery/classifier.ts +127 -0
  59. package/src/recovery/event-handler.ts +263 -0
  60. package/src/recovery/index.ts +20 -0
  61. package/src/recovery/orchestrator.ts +180 -0
  62. package/src/recovery/persistence.ts +87 -0
  63. package/src/recovery/strategies.ts +107 -0
  64. package/src/recovery/types.ts +31 -0
  65. package/src/registry/model-groups.ts +2 -19
  66. package/src/registry/resolver.ts +38 -9
  67. package/src/review/agent-catalog.ts +83 -251
  68. package/src/review/agents/architecture-verifier.ts +41 -0
  69. package/src/review/agents/code-hygiene-auditor.ts +40 -0
  70. package/src/review/agents/correctness-auditor.ts +41 -0
  71. package/src/review/agents/frontend-auditor.ts +39 -0
  72. package/src/review/agents/index.ts +15 -42
  73. package/src/review/agents/language-idioms-auditor.ts +39 -0
  74. package/src/review/agents/security-auditor.ts +12 -8
  75. package/src/review/stack-gate.ts +2 -6
  76. package/src/routing/categories.ts +111 -0
  77. package/src/routing/classifier.ts +152 -0
  78. package/src/routing/engine.ts +89 -0
  79. package/src/routing/index.ts +4 -0
  80. package/src/routing/types.ts +14 -0
  81. package/src/skills/adaptive-injector.ts +34 -3
  82. package/src/skills/loader.ts +4 -0
  83. package/src/tools/background.ts +196 -0
  84. package/src/tools/configure.ts +1 -1
  85. package/src/tools/delegate.ts +205 -0
  86. package/src/tools/loop.ts +94 -0
  87. package/src/tools/recover.ts +172 -0
  88. package/src/types/background.ts +51 -0
  89. package/src/types/mcp.ts +27 -0
  90. package/src/types/recovery.ts +49 -0
  91. package/src/types/routing.ts +39 -0
  92. package/src/ux/context-warnings.ts +81 -0
  93. package/src/ux/error-hints.ts +38 -0
  94. package/src/ux/index.ts +7 -0
  95. package/src/ux/notifications.ts +67 -0
  96. package/src/ux/progress.ts +77 -0
  97. package/src/ux/session-summary.ts +67 -0
  98. package/src/ux/task-status.ts +109 -0
  99. package/src/ux/types.ts +24 -0
  100. package/src/agents/db-specialist.ts +0 -295
  101. package/src/agents/devops.ts +0 -352
  102. package/src/agents/documenter.ts +0 -44
  103. package/src/agents/frontend-engineer.ts +0 -541
  104. package/src/agents/pipeline/oc-explorer.ts +0 -46
  105. package/src/agents/pipeline/oc-retrospector.ts +0 -42
  106. package/src/review/agents/auth-flow-verifier.ts +0 -47
  107. package/src/review/agents/concurrency-checker.ts +0 -47
  108. package/src/review/agents/dead-code-scanner.ts +0 -47
  109. package/src/review/agents/go-idioms-auditor.ts +0 -46
  110. package/src/review/agents/python-django-auditor.ts +0 -46
  111. package/src/review/agents/react-patterns-auditor.ts +0 -46
  112. package/src/review/agents/rust-safety-auditor.ts +0 -46
  113. package/src/review/agents/scope-intent-verifier.ts +0 -45
  114. package/src/review/agents/silent-failure-hunter.ts +0 -45
  115. package/src/review/agents/spec-checker.ts +0 -45
  116. package/src/review/agents/state-mgmt-auditor.ts +0 -46
  117. package/src/review/agents/type-soundness.ts +0 -46
  118. 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 defaultTimeout: number;
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
- defaultTimeout: 300000,
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
- defaultTimeout: 300000,
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
- // Export aliases updated to v6
186
- export const pluginConfigSchema = pluginConfigSchemaV6;
189
+ type PluginConfigV6 = z.infer<typeof pluginConfigSchemaV6>;
187
190
 
188
- export type PluginConfig = z.infer<typeof pluginConfigSchemaV6>;
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 CONFIG_PATH = join(getGlobalConfigDir(), "opencode-autopilot.json");
225
+ export const pluginConfigSchema = pluginConfigSchemaV7;
226
+
227
+ export type PluginConfig = z.infer<typeof pluginConfigSchemaV7>;
191
228
 
192
- // --- Migration ---
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): PluginConfig {
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
- // --- Public API ---
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
- // Try v6 first
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) return v6Result.data;
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 migrated = migrateV5toV6(v5Result.data);
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 migrated = migrateV5toV6(v5);
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 migrated = migrateV5toV6(v5);
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 migrated = migrateV5toV6(v5);
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 migrated = migrateV5toV6(v5);
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 pluginConfigSchemaV6.parse(parsed); // throw with proper error
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: 6 as const,
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
+ }