@kodrunhq/opencode-autopilot 1.18.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 (110) hide show
  1. package/README.md +95 -13
  2. package/assets/commands/oc-update-docs.md +1 -1
  3. package/package.json +1 -1
  4. package/src/agents/index.ts +0 -12
  5. package/src/agents/pipeline/index.ts +0 -4
  6. package/src/autonomy/completion.ts +52 -0
  7. package/src/autonomy/controller.ts +144 -0
  8. package/src/autonomy/index.ts +25 -0
  9. package/src/autonomy/injector.ts +49 -0
  10. package/src/autonomy/state.ts +91 -0
  11. package/src/autonomy/types.ts +30 -0
  12. package/src/autonomy/verification.ts +86 -0
  13. package/src/background/database.ts +170 -0
  14. package/src/background/executor.ts +174 -0
  15. package/src/background/index.ts +8 -0
  16. package/src/background/manager.ts +232 -0
  17. package/src/background/repository.ts +174 -0
  18. package/src/background/schema.ts +24 -0
  19. package/src/background/sdk-runner.ts +40 -0
  20. package/src/background/slot-manager.ts +41 -0
  21. package/src/background/state-machine.ts +19 -0
  22. package/src/context/budget.ts +45 -0
  23. package/src/context/compaction-handler.ts +58 -0
  24. package/src/context/discovery.ts +94 -0
  25. package/src/context/index.ts +14 -0
  26. package/src/context/injector.ts +119 -0
  27. package/src/context/types.ts +24 -0
  28. package/src/health/checks.ts +145 -2
  29. package/src/health/index.ts +7 -1
  30. package/src/health/runner.ts +6 -0
  31. package/src/index.ts +113 -6
  32. package/src/installer.ts +13 -0
  33. package/src/kernel/index.ts +6 -0
  34. package/src/kernel/migrations.ts +50 -0
  35. package/src/kernel/retry.ts +49 -0
  36. package/src/kernel/schema.ts +9 -1
  37. package/src/kernel/transaction.ts +40 -12
  38. package/src/logging/forensic-writer.ts +6 -2
  39. package/src/logging/index.ts +2 -0
  40. package/src/mcp/index.ts +34 -0
  41. package/src/mcp/manager.ts +206 -0
  42. package/src/mcp/scope-filter.ts +44 -0
  43. package/src/mcp/types.ts +38 -0
  44. package/src/orchestrator/arena.ts +7 -1
  45. package/src/orchestrator/fallback/event-handler.ts +12 -1
  46. package/src/orchestrator/handlers/challenge.ts +8 -1
  47. package/src/orchestrator/handlers/plan.ts +8 -1
  48. package/src/orchestrator/handlers/recon.ts +8 -1
  49. package/src/orchestrator/handlers/types.ts +2 -2
  50. package/src/orchestrator/lesson-memory.ts +6 -1
  51. package/src/orchestrator/orchestration-logger.ts +15 -3
  52. package/src/orchestrator/skill-injection.ts +7 -1
  53. package/src/orchestrator/state.ts +6 -1
  54. package/src/recovery/classifier.ts +127 -0
  55. package/src/recovery/event-handler.ts +263 -0
  56. package/src/recovery/index.ts +20 -0
  57. package/src/recovery/orchestrator.ts +180 -0
  58. package/src/recovery/persistence.ts +87 -0
  59. package/src/recovery/strategies.ts +107 -0
  60. package/src/recovery/types.ts +31 -0
  61. package/src/registry/model-groups.ts +2 -19
  62. package/src/registry/resolver.ts +38 -9
  63. package/src/review/agent-catalog.ts +83 -251
  64. package/src/review/agents/architecture-verifier.ts +41 -0
  65. package/src/review/agents/code-hygiene-auditor.ts +40 -0
  66. package/src/review/agents/correctness-auditor.ts +41 -0
  67. package/src/review/agents/frontend-auditor.ts +39 -0
  68. package/src/review/agents/index.ts +15 -42
  69. package/src/review/agents/language-idioms-auditor.ts +39 -0
  70. package/src/review/agents/security-auditor.ts +12 -8
  71. package/src/review/stack-gate.ts +2 -6
  72. package/src/routing/categories.ts +111 -0
  73. package/src/routing/classifier.ts +152 -0
  74. package/src/routing/engine.ts +89 -0
  75. package/src/routing/index.ts +4 -0
  76. package/src/routing/types.ts +14 -0
  77. package/src/skills/adaptive-injector.ts +34 -3
  78. package/src/skills/loader.ts +4 -0
  79. package/src/tools/background.ts +196 -0
  80. package/src/tools/delegate.ts +205 -0
  81. package/src/tools/loop.ts +94 -0
  82. package/src/tools/recover.ts +172 -0
  83. package/src/types/recovery.ts +10 -0
  84. package/src/ux/context-warnings.ts +81 -0
  85. package/src/ux/error-hints.ts +38 -0
  86. package/src/ux/index.ts +7 -0
  87. package/src/ux/notifications.ts +67 -0
  88. package/src/ux/progress.ts +77 -0
  89. package/src/ux/session-summary.ts +67 -0
  90. package/src/ux/task-status.ts +109 -0
  91. package/src/ux/types.ts +24 -0
  92. package/src/agents/db-specialist.ts +0 -295
  93. package/src/agents/devops.ts +0 -352
  94. package/src/agents/documenter.ts +0 -44
  95. package/src/agents/frontend-engineer.ts +0 -541
  96. package/src/agents/pipeline/oc-explorer.ts +0 -46
  97. package/src/agents/pipeline/oc-retrospector.ts +0 -42
  98. package/src/review/agents/auth-flow-verifier.ts +0 -47
  99. package/src/review/agents/concurrency-checker.ts +0 -47
  100. package/src/review/agents/dead-code-scanner.ts +0 -47
  101. package/src/review/agents/go-idioms-auditor.ts +0 -46
  102. package/src/review/agents/python-django-auditor.ts +0 -46
  103. package/src/review/agents/react-patterns-auditor.ts +0 -46
  104. package/src/review/agents/rust-safety-auditor.ts +0 -46
  105. package/src/review/agents/scope-intent-verifier.ts +0 -45
  106. package/src/review/agents/silent-failure-hunter.ts +0 -45
  107. package/src/review/agents/spec-checker.ts +0 -45
  108. package/src/review/agents/state-mgmt-auditor.ts +0 -46
  109. package/src/review/agents/type-soundness.ts +0 -46
  110. package/src/review/agents/wiring-inspector.ts +0 -46
@@ -1,10 +1,13 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import { access, readFile, stat } from "node:fs/promises";
3
- import { join } from "node:path";
3
+ import { dirname, join } from "node:path";
4
4
  import type { Config } from "@opencode-ai/plugin";
5
5
  import { parse } from "yaml";
6
6
  import { loadConfig } from "../config";
7
+ import { getGlobalMcpManager } from "../mcp";
7
8
  import { AGENT_NAMES } from "../orchestrator/handlers/types";
9
+ import { ALL_GROUP_IDS } from "../registry/model-groups";
10
+ import { getAllCategories } from "../routing";
8
11
  import { detectProjectStackTags, filterSkillsByStack } from "../skills/adaptive-injector";
9
12
  import { loadAllSkills } from "../skills/loader";
10
13
  import {
@@ -15,6 +18,8 @@ import {
15
18
  } from "../utils/paths";
16
19
  import type { HealthResult } from "./types";
17
20
 
21
+ const VALID_CATEGORY_NAMES = new Set(getAllCategories().map((definition) => definition.category));
22
+
18
23
  /**
19
24
  * Check that the plugin config file exists and passes Zod validation.
20
25
  * loadConfig returns null when the file is missing, and throws on invalid JSON/schema.
@@ -213,9 +218,13 @@ export async function configV7FieldsCheck(configPath?: string): Promise<HealthRe
213
218
  const STANDARD_AGENT_NAMES: readonly string[] = Object.freeze([
214
219
  "researcher",
215
220
  "metaprompter",
216
- "documenter",
217
221
  "pr-reviewer",
218
222
  "autopilot",
223
+ "coder",
224
+ "debugger",
225
+ "planner",
226
+ "reviewer",
227
+ "security-auditor",
219
228
  ]);
220
229
 
221
230
  /** Pipeline agent names, derived from AGENT_NAMES in the orchestrator. */
@@ -410,6 +419,88 @@ export async function skillHealthCheck(
410
419
  }
411
420
  }
412
421
 
422
+ export async function mcpHealthCheck(configPath?: string): Promise<HealthResult> {
423
+ try {
424
+ const config = await loadConfig(configPath);
425
+ if (config === null) {
426
+ return Object.freeze({
427
+ name: "mcp-health",
428
+ status: "fail" as const,
429
+ message: "Plugin config file not found",
430
+ });
431
+ }
432
+
433
+ if (!config.mcp.enabled) {
434
+ return Object.freeze({
435
+ name: "mcp-health",
436
+ status: "pass" as const,
437
+ message: "MCP disabled in config",
438
+ });
439
+ }
440
+
441
+ const manager = getGlobalMcpManager();
442
+ if (manager) {
443
+ const healthResults = await manager.healthCheckAll();
444
+ if (healthResults.length === 0) {
445
+ return Object.freeze({
446
+ name: "mcp-health",
447
+ status: "pass" as const,
448
+ message: "MCP enabled, no servers running",
449
+ });
450
+ }
451
+
452
+ const unhealthy = healthResults.filter((result) => result.state !== "healthy");
453
+ const details = Object.freeze(
454
+ healthResults.map((result) => {
455
+ const status = result.state === "healthy" ? "ok" : result.state;
456
+ const errorSuffix = result.error ? ` - ${result.error}` : "";
457
+ return `${result.serverName} (${result.skillName}): ${status}${errorSuffix}`;
458
+ }),
459
+ );
460
+
461
+ if (unhealthy.length > 0) {
462
+ return Object.freeze({
463
+ name: "mcp-health",
464
+ status: "warn" as const,
465
+ message: `MCP enabled: ${healthResults.length} server(s), ${unhealthy.length} unhealthy`,
466
+ details,
467
+ });
468
+ }
469
+
470
+ return Object.freeze({
471
+ name: "mcp-health",
472
+ status: "pass" as const,
473
+ message: `MCP enabled: ${healthResults.length} server(s) healthy`,
474
+ details,
475
+ });
476
+ }
477
+
478
+ const skillsDir = join(configPath ? dirname(configPath) : getGlobalConfigDir(), "skills");
479
+ const skills = await loadAllSkills(skillsDir);
480
+ const mcpSkills = [...skills.values()].filter((skill) => skill.frontmatter.mcp !== null);
481
+ const details = Object.freeze(
482
+ mcpSkills.map((skill) => {
483
+ const mcp = skill.frontmatter.mcp;
484
+ return `${skill.frontmatter.name}: ${mcp?.serverName} (${mcp?.transport})`;
485
+ }),
486
+ );
487
+
488
+ return Object.freeze({
489
+ name: "mcp-health",
490
+ status: "pass" as const,
491
+ message: `MCP enabled with ${mcpSkills.length} MCP-capable skill${mcpSkills.length === 1 ? "" : "s"} (manager not initialized)`,
492
+ details,
493
+ });
494
+ } catch (error: unknown) {
495
+ const msg = error instanceof Error ? error.message : String(error);
496
+ return Object.freeze({
497
+ name: "mcp-health",
498
+ status: "fail" as const,
499
+ message: `MCP health check failed: ${msg}`,
500
+ });
501
+ }
502
+ }
503
+
413
504
  /**
414
505
  * Check memory DB health: existence, readability, observation count.
415
506
  * Does NOT call getMemoryDb() to avoid creating an empty DB as a side effect.
@@ -573,3 +664,55 @@ export async function commandHealthCheck(targetDir?: string): Promise<HealthResu
573
664
  details: Object.freeze(issues),
574
665
  });
575
666
  }
667
+
668
+ export async function routingHealthCheck(configPath?: string): Promise<HealthResult> {
669
+ try {
670
+ const invalidDefinitions = getAllCategories().flatMap((definition) => {
671
+ const issues: string[] = [];
672
+ if (!VALID_CATEGORY_NAMES.has(definition.category)) {
673
+ issues.push(`${definition.category}: unknown category`);
674
+ }
675
+ if (!ALL_GROUP_IDS.includes(definition.modelGroup as (typeof ALL_GROUP_IDS)[number])) {
676
+ issues.push(`${definition.category}: invalid model group '${definition.modelGroup}'`);
677
+ }
678
+ if (definition.maxIterations < 1) {
679
+ issues.push(`${definition.category}: maxIterations must be >= 1`);
680
+ }
681
+ if (definition.timeoutSeconds < 1) {
682
+ issues.push(`${definition.category}: timeoutSeconds must be >= 1`);
683
+ }
684
+ return issues;
685
+ });
686
+
687
+ const config = await loadConfig(configPath);
688
+ const invalidOverrides = Object.keys(config?.routing.categories ?? {}).flatMap(
689
+ (categoryName) =>
690
+ VALID_CATEGORY_NAMES.has(categoryName as import("../types/routing").Category)
691
+ ? []
692
+ : [`Invalid routing override category '${categoryName}'`],
693
+ );
694
+
695
+ const issues = [...invalidDefinitions, ...invalidOverrides];
696
+ if (issues.length > 0) {
697
+ return Object.freeze({
698
+ name: "routing-health",
699
+ status: "fail" as const,
700
+ message: `${issues.length} routing issue(s) found`,
701
+ details: Object.freeze(issues),
702
+ });
703
+ }
704
+
705
+ return Object.freeze({
706
+ name: "routing-health",
707
+ status: "pass" as const,
708
+ message: `All ${getAllCategories().length} routing categories and overrides are valid`,
709
+ });
710
+ } catch (error: unknown) {
711
+ const msg = error instanceof Error ? error.message : String(error);
712
+ return Object.freeze({
713
+ name: "routing-health",
714
+ status: "fail" as const,
715
+ message: `Routing health check failed: ${msg}`,
716
+ });
717
+ }
718
+ }
@@ -1,3 +1,9 @@
1
- export { agentHealthCheck, assetHealthCheck, configHealthCheck } from "./checks";
1
+ export {
2
+ agentHealthCheck,
3
+ assetHealthCheck,
4
+ configHealthCheck,
5
+ mcpHealthCheck,
6
+ routingHealthCheck,
7
+ } from "./checks";
2
8
  export { runHealthChecks } from "./runner";
3
9
  export type { HealthReport, HealthResult } from "./types";
@@ -5,8 +5,10 @@ import {
5
5
  commandHealthCheck,
6
6
  configHealthCheck,
7
7
  configV7FieldsCheck,
8
+ mcpHealthCheck,
8
9
  memoryHealthCheck,
9
10
  nativeAgentSuppressionHealthCheck,
11
+ routingHealthCheck,
10
12
  skillHealthCheck,
11
13
  } from "./checks";
12
14
  import type { HealthReport, HealthResult } from "./types";
@@ -54,6 +56,8 @@ export async function runHealthChecks(options?: {
54
56
  memoryHealthCheck(options?.targetDir),
55
57
  commandHealthCheck(options?.targetDir),
56
58
  configV7FieldsCheck(options?.configPath),
59
+ routingHealthCheck(options?.configPath),
60
+ mcpHealthCheck(options?.configPath),
57
61
  ]);
58
62
 
59
63
  const allSettled = [...configOutcome, ...settled];
@@ -67,6 +71,8 @@ export async function runHealthChecks(options?: {
67
71
  "memory-db",
68
72
  "command-accessibility",
69
73
  "config-v7-fields",
74
+ "routing-health",
75
+ "mcp-health",
70
76
  ];
71
77
  const results: readonly HealthResult[] = Object.freeze(
72
78
  allSettled.map((outcome, i) => settledToResult(outcome, fallbackNames[i])),
package/src/index.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import type { Config, Plugin } from "@opencode-ai/plugin";
2
2
  import { configHook } from "./agents";
3
+ import { getLoopController } from "./autonomy";
4
+ import { createLoopInjector } from "./autonomy/injector";
3
5
  import { isFirstLoad, loadConfig } from "./config";
6
+ import { createCompactionHandler, createContextInjector } from "./context";
4
7
  import { runHealthChecks } from "./health/runner";
5
8
  import { createAntiSlopHandler } from "./hooks/anti-slop";
6
9
  import { installAssets } from "./installer";
10
+ import { openKernelDb } from "./kernel/database";
7
11
  import { getLogger, initLoggers } from "./logging/domains";
12
+ import { McpLifecycleManager, setGlobalMcpManager } from "./mcp";
8
13
  import {
9
14
  createMemoryCaptureHandler,
10
15
  createMemoryChatMessageHandler,
@@ -30,6 +35,12 @@ import {
30
35
  } from "./orchestrator/fallback";
31
36
  import { fallbackDefaults } from "./orchestrator/fallback/fallback-config";
32
37
  import { resolveChain } from "./orchestrator/fallback/resolve-chain";
38
+ import {
39
+ createRecoveryEventHandler,
40
+ createRecoveryOrchestratorWithDb,
41
+ getDefaultRecoveryOrchestrator,
42
+ } from "./recovery/index";
43
+ import { ocBackground, setBackgroundSdkOperations } from "./tools/background";
33
44
  import { ocConfidence } from "./tools/confidence";
34
45
  import {
35
46
  ocConfigure,
@@ -40,10 +51,12 @@ import {
40
51
  import { ocCreateAgent } from "./tools/create-agent";
41
52
  import { ocCreateCommand } from "./tools/create-command";
42
53
  import { ocCreateSkill } from "./tools/create-skill";
54
+ import { ocDelegate, setDelegateSdkOperations } from "./tools/delegate";
43
55
  import { ocDoctor, setOpenCodeConfig as setDoctorOpenCodeConfig } from "./tools/doctor";
44
56
  import { ocForensics } from "./tools/forensics";
45
57
  import { ocHashlineEdit } from "./tools/hashline-edit";
46
58
  import { ocLogs } from "./tools/logs";
59
+ import { ocLoop } from "./tools/loop";
47
60
  import { ocMemoryPreferences } from "./tools/memory-preferences";
48
61
  import { ocMemoryStatus } from "./tools/memory-status";
49
62
  import { ocMockFallback } from "./tools/mock-fallback";
@@ -52,12 +65,17 @@ import { ocPhase } from "./tools/phase";
52
65
  import { ocPipelineReport } from "./tools/pipeline-report";
53
66
  import { ocPlan } from "./tools/plan";
54
67
  import { ocQuick } from "./tools/quick";
68
+ import { ocRecover } from "./tools/recover";
55
69
  import { ocReview } from "./tools/review";
56
70
  import { ocSessionStats } from "./tools/session-stats";
57
71
  import { ocState } from "./tools/state";
58
72
  import { ocStocktake } from "./tools/stocktake";
59
73
  import { ocSummary } from "./tools/summary";
60
74
  import { ocUpdateDocs } from "./tools/update-docs";
75
+ import { ContextWarningMonitor } from "./ux/context-warnings";
76
+ import { getRemediationHint } from "./ux/error-hints";
77
+ import { NotificationManager } from "./ux/notifications";
78
+ import { ProgressTracker } from "./ux/progress";
61
79
 
62
80
  let openCodeConfig: Config | null = null;
63
81
 
@@ -127,6 +145,20 @@ const plugin: Plugin = async (input) => {
127
145
  });
128
146
  });
129
147
 
148
+ // --- UX notification manager (rate-limited, best-effort) ---
149
+ const notificationManager = new NotificationManager({
150
+ sink: {
151
+ showToast: async (title, message, variant, duration) => {
152
+ await sdkOps.showToast(title, message, variant as "info" | "warning" | "error");
153
+ void duration;
154
+ },
155
+ },
156
+ });
157
+
158
+ // --- UX surfaces: context warnings, progress tracking, error hints ---
159
+ const contextWarningMonitor = new ContextWarningMonitor({ notificationManager });
160
+ const _progressTracker = new ProgressTracker({ notificationManager });
161
+
130
162
  // --- Fallback subsystem initialization ---
131
163
  const sdkOps: SdkOperations = {
132
164
  abortSession: async (sessionID) => {
@@ -160,6 +192,24 @@ const plugin: Plugin = async (input) => {
160
192
  },
161
193
  };
162
194
 
195
+ // --- Background task SDK wiring (enables real dispatch via promptAsync) ---
196
+ const backgroundSdkOps = {
197
+ promptAsync: async (
198
+ sessionId: string,
199
+ model: string | undefined,
200
+ parts: ReadonlyArray<{ type: "text"; text: string }>,
201
+ ) => {
202
+ const modelSpec = model ? { providerID: "", modelID: model } : undefined;
203
+ await sdkOps.promptAsync(
204
+ sessionId,
205
+ modelSpec as { readonly providerID: string; readonly modelID: string },
206
+ parts as readonly import("./orchestrator/fallback").MessagePart[],
207
+ );
208
+ },
209
+ };
210
+ setBackgroundSdkOperations(backgroundSdkOps);
211
+ setDelegateSdkOperations(backgroundSdkOps);
212
+
163
213
  const manager = new FallbackManager({
164
214
  config: fallbackConfig,
165
215
  resolveFallbackChain: (_sessionID, agentName) => {
@@ -202,6 +252,23 @@ const plugin: Plugin = async (input) => {
202
252
  });
203
253
  const chatMessageHandler = createChatMessageHandler(manager);
204
254
  const toolExecuteAfterHandler = createToolExecuteAfterHandler(manager);
255
+ const recoveryEventHandler = (() => {
256
+ try {
257
+ const kernelDb = openKernelDb();
258
+ const orchestrator = createRecoveryOrchestratorWithDb(kernelDb);
259
+ return createRecoveryEventHandler({
260
+ orchestrator,
261
+ db: kernelDb,
262
+ sdk: {
263
+ abortSession: sdkOps.abortSession,
264
+ showToast: (title, message, variant) =>
265
+ sdkOps.showToast(title, message, variant as "info" | "warning" | "error"),
266
+ },
267
+ });
268
+ } catch {
269
+ return createRecoveryEventHandler(getDefaultRecoveryOrchestrator());
270
+ }
271
+ })();
205
272
 
206
273
  // --- Anti-slop hook initialization ---
207
274
  const antiSlopHandler = createAntiSlopHandler({ showToast: sdkOps.showToast });
@@ -228,6 +295,16 @@ const plugin: Plugin = async (input) => {
228
295
  getDb: () => getMemoryDb(),
229
296
  })
230
297
  : null;
298
+ const contextInjector = createContextInjector({
299
+ projectRoot: process.cwd(),
300
+ totalBudget: 4000,
301
+ });
302
+ const compactionHandler = createCompactionHandler(contextInjector);
303
+ const loopInjector = createLoopInjector(getLoopController());
304
+
305
+ // --- MCP lifecycle manager (lazy — servers start when skills with mcp: config activate) ---
306
+ const mcpManager = new McpLifecycleManager();
307
+ setGlobalMcpManager(mcpManager);
231
308
 
232
309
  // --- Observability handlers ---
233
310
  const toolStartTimes = new Map<string, number>();
@@ -312,7 +389,9 @@ const plugin: Plugin = async (input) => {
312
389
 
313
390
  return {
314
391
  tool: {
392
+ oc_background: ocBackground,
315
393
  oc_configure: ocConfigure,
394
+ oc_delegate: ocDelegate,
316
395
  oc_create_agent: ocCreateAgent,
317
396
  oc_create_skill: ocCreateSkill,
318
397
  oc_create_command: ocCreateCommand,
@@ -323,10 +402,12 @@ const plugin: Plugin = async (input) => {
323
402
  oc_orchestrate: ocOrchestrate,
324
403
  oc_doctor: ocDoctor,
325
404
  oc_quick: ocQuick,
405
+ oc_recover: ocRecover,
326
406
  oc_forensics: ocForensics,
327
407
  oc_hashline_edit: ocHashlineEdit,
328
408
  oc_review: ocReview,
329
409
  oc_logs: ocLogs,
410
+ oc_loop: ocLoop,
330
411
  oc_session_stats: ocSessionStats,
331
412
  oc_pipeline_report: ocPipelineReport,
332
413
  oc_summary: ocSummary,
@@ -337,10 +418,8 @@ const plugin: Plugin = async (input) => {
337
418
  oc_memory_preferences: ocMemoryPreferences,
338
419
  },
339
420
  event: async ({ event }) => {
340
- // 1. Observability: collect (pure observer, no side effects on session)
341
421
  await observabilityEventHandler({ event });
342
422
 
343
- // 2. Memory capture (pure observer, best-effort)
344
423
  if (memoryCaptureHandler) {
345
424
  try {
346
425
  await memoryCaptureHandler({ event });
@@ -349,19 +428,45 @@ const plugin: Plugin = async (input) => {
349
428
  }
350
429
  }
351
430
 
352
- // 3. First-load toast
353
431
  if (event.type === "session.created" && isFirstLoad(config)) {
354
- await sdkOps.showToast(
432
+ await notificationManager.info(
355
433
  "Welcome to OpenCode Autopilot!",
356
434
  "Plugin loaded. Run oc_doctor to verify your setup.",
357
- "info",
358
435
  );
359
436
  }
360
437
 
361
- // 4. Fallback event handling
438
+ if (event.type === "session.error") {
439
+ const props = event.properties;
440
+ const errorMsg =
441
+ props && typeof props === "object" && "error" in props
442
+ ? String((props as Record<string, unknown>).error)
443
+ : "Unknown error";
444
+ const hint = getRemediationHint(errorMsg);
445
+ const displayMsg = hint ? `${errorMsg}\n${hint}` : errorMsg;
446
+ await notificationManager.error("Session Error", displayMsg);
447
+ }
448
+
449
+ if (event.type === "message.updated") {
450
+ const props = (event.properties ?? {}) as Record<string, unknown>;
451
+ const info = props.info as Record<string, unknown> | undefined;
452
+ if (info) {
453
+ const tokens = info.tokens as { input?: number } | undefined;
454
+ if (tokens && typeof tokens.input === "number") {
455
+ contextWarningMonitor.checkUtilization(tokens.input, 200_000);
456
+ }
457
+ }
458
+ }
459
+
362
460
  if (fallbackConfig.enabled) {
363
461
  await fallbackEventHandler({ event });
364
462
  }
463
+
464
+ await recoveryEventHandler({ event });
465
+ await compactionHandler({ event });
466
+
467
+ if (event.type === "session.deleted") {
468
+ mcpManager.stopAll().catch(() => {});
469
+ }
365
470
  },
366
471
  config: async (cfg: Config) => {
367
472
  openCodeConfig = cfg;
@@ -422,6 +527,8 @@ const plugin: Plugin = async (input) => {
422
527
  if (memoryInjector) {
423
528
  await memoryInjector(input, output);
424
529
  }
530
+ await contextInjector(input, output);
531
+ await loopInjector(input, output);
425
532
  },
426
533
  };
427
534
  };
package/src/installer.ts CHANGED
@@ -9,6 +9,19 @@ import { getAssetsDir, getGlobalConfigDir } from "./utils/paths";
9
9
  */
10
10
  const DEPRECATED_ASSETS = [
11
11
  "agents/placeholder-agent.md",
12
+ "agents/auth-flow-verifier.md",
13
+ "agents/concurrency-checker.md",
14
+ "agents/dead-code-scanner.md",
15
+ "agents/go-idioms-auditor.md",
16
+ "agents/python-django-auditor.md",
17
+ "agents/react-patterns-auditor.md",
18
+ "agents/rust-safety-auditor.md",
19
+ "agents/scope-intent-verifier.md",
20
+ "agents/silent-failure-hunter.md",
21
+ "agents/spec-checker.md",
22
+ "agents/state-mgmt-auditor.md",
23
+ "agents/type-soundness.md",
24
+ "agents/wiring-inspector.md",
12
25
  "commands/configure.md",
13
26
  "commands/oc-configure.md",
14
27
  "commands/brainstorm.md",
@@ -0,0 +1,6 @@
1
+ export { getKernelDbPath, KERNEL_DB_FILE, kernelDbExists, openKernelDb } from "./database";
2
+ export { runKernelMigrations } from "./migrations";
3
+ export { type RetryOptions, withRetry } from "./retry";
4
+ export * from "./schema";
5
+ export * from "./transaction";
6
+ export * from "./types";
@@ -9,6 +9,13 @@ function columnExists(database: Database, tableName: string, columnName: string)
9
9
  return columns.some((column) => column.name === columnName);
10
10
  }
11
11
 
12
+ function tableExists(database: Database, tableName: string): boolean {
13
+ const row = database
14
+ .query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
15
+ .get(tableName) as { name?: string } | null;
16
+ return row?.name === tableName;
17
+ }
18
+
12
19
  function backfillProjectAwareColumns(database: Database): void {
13
20
  if (!columnExists(database, "pipeline_runs", "project_id")) {
14
21
  database.run("ALTER TABLE pipeline_runs ADD COLUMN project_id TEXT");
@@ -44,6 +51,48 @@ function backfillProjectAwareColumns(database: Database): void {
44
51
  }
45
52
  }
46
53
 
54
+ function backfillBackgroundTaskColumns(database: Database): void {
55
+ if (!tableExists(database, "background_tasks")) {
56
+ return;
57
+ }
58
+
59
+ const columnDefinitions = Object.freeze([
60
+ { name: "category", ddl: "ALTER TABLE background_tasks ADD COLUMN category TEXT" },
61
+ { name: "result", ddl: "ALTER TABLE background_tasks ADD COLUMN result TEXT" },
62
+ { name: "error", ddl: "ALTER TABLE background_tasks ADD COLUMN error TEXT" },
63
+ { name: "agent", ddl: "ALTER TABLE background_tasks ADD COLUMN agent TEXT" },
64
+ { name: "model", ddl: "ALTER TABLE background_tasks ADD COLUMN model TEXT" },
65
+ {
66
+ name: "priority",
67
+ ddl: "ALTER TABLE background_tasks ADD COLUMN priority INTEGER NOT NULL DEFAULT 50",
68
+ },
69
+ { name: "created_at", ddl: "ALTER TABLE background_tasks ADD COLUMN created_at TEXT" },
70
+ { name: "updated_at", ddl: "ALTER TABLE background_tasks ADD COLUMN updated_at TEXT" },
71
+ { name: "started_at", ddl: "ALTER TABLE background_tasks ADD COLUMN started_at TEXT" },
72
+ { name: "completed_at", ddl: "ALTER TABLE background_tasks ADD COLUMN completed_at TEXT" },
73
+ ]);
74
+
75
+ for (const column of columnDefinitions) {
76
+ if (!columnExists(database, "background_tasks", column.name)) {
77
+ database.run(column.ddl);
78
+ }
79
+ }
80
+
81
+ const now = new Date().toISOString();
82
+ if (columnExists(database, "background_tasks", "created_at")) {
83
+ database.run(
84
+ "UPDATE background_tasks SET created_at = COALESCE(created_at, ?) WHERE created_at IS NULL",
85
+ [now],
86
+ );
87
+ }
88
+ if (columnExists(database, "background_tasks", "updated_at")) {
89
+ database.run(
90
+ "UPDATE background_tasks SET updated_at = COALESCE(updated_at, created_at, ?) WHERE updated_at IS NULL",
91
+ [now],
92
+ );
93
+ }
94
+ }
95
+
47
96
  export function runKernelMigrations(database: Database): void {
48
97
  const row = database.query("PRAGMA user_version").get() as { user_version?: number } | null;
49
98
  const currentVersion = row?.user_version ?? 0;
@@ -55,6 +104,7 @@ export function runKernelMigrations(database: Database): void {
55
104
  }
56
105
 
57
106
  backfillProjectAwareColumns(database);
107
+ backfillBackgroundTaskColumns(database);
58
108
 
59
109
  if (currentVersion < KERNEL_SCHEMA_VERSION) {
60
110
  database.run(`PRAGMA user_version = ${KERNEL_SCHEMA_VERSION}`);
@@ -0,0 +1,49 @@
1
+ export interface RetryOptions {
2
+ readonly maxRetries?: number;
3
+ readonly backoffMs?: number;
4
+ readonly onRetry?: (attempt: number, error: Error) => void;
5
+ }
6
+
7
+ const DEFAULT_MAX_RETRIES = 3;
8
+ const DEFAULT_BACKOFF_MS = 100;
9
+
10
+ function isBusyError(error: Error): boolean {
11
+ return (
12
+ error.message.includes("database is locked") ||
13
+ error.message.includes("SQLITE_BUSY") ||
14
+ error.message.includes("database table is locked")
15
+ );
16
+ }
17
+
18
+ function sleep(ms: number): Promise<void> {
19
+ return new Promise((resolve) => {
20
+ setTimeout(resolve, ms);
21
+ });
22
+ }
23
+
24
+ export async function withRetry<T>(
25
+ fn: () => T | Promise<T>,
26
+ options: RetryOptions = {},
27
+ ): Promise<T> {
28
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
29
+ const backoffMs = options.backoffMs ?? DEFAULT_BACKOFF_MS;
30
+
31
+ let attempt = 0;
32
+ while (true) {
33
+ try {
34
+ return await fn();
35
+ } catch (error: unknown) {
36
+ if (!(error instanceof Error) || !isBusyError(error)) {
37
+ throw error;
38
+ }
39
+
40
+ if (attempt >= maxRetries) {
41
+ throw error;
42
+ }
43
+
44
+ attempt += 1;
45
+ options.onRetry?.(attempt, error);
46
+ await sleep(backoffMs * 2 ** (attempt - 1));
47
+ }
48
+ }
49
+ }
@@ -1,4 +1,6 @@
1
- export const KERNEL_SCHEMA_VERSION = 1;
1
+ import { BACKGROUND_TASKS_SCHEMA_STATEMENTS } from "../background/schema";
2
+
3
+ export const KERNEL_SCHEMA_VERSION = 3;
2
4
 
3
5
  export const KERNEL_SCHEMA_STATEMENTS: readonly string[] = Object.freeze([
4
6
  `CREATE TABLE IF NOT EXISTS pipeline_runs (
@@ -119,4 +121,10 @@ export const KERNEL_SCHEMA_STATEMENTS: readonly string[] = Object.freeze([
119
121
  `CREATE INDEX IF NOT EXISTS idx_forensic_events_run ON forensic_events(run_id, timestamp, event_id)`,
120
122
  `CREATE INDEX IF NOT EXISTS idx_forensic_events_dispatch ON forensic_events(dispatch_id, timestamp, event_id)`,
121
123
  `CREATE INDEX IF NOT EXISTS idx_forensic_events_type ON forensic_events(type, timestamp, event_id)`,
124
+ `CREATE TABLE IF NOT EXISTS recovery_state (
125
+ session_id TEXT PRIMARY KEY,
126
+ state_json TEXT NOT NULL,
127
+ updated_at TEXT NOT NULL
128
+ )`,
129
+ ...BACKGROUND_TASKS_SCHEMA_STATEMENTS,
122
130
  ]);