@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
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
|
|
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
|
-
|
|
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";
|
package/src/kernel/migrations.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/kernel/schema.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
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
|
]);
|
|
@@ -6,6 +6,29 @@ export interface TransactionOptions {
|
|
|
6
6
|
useImmediate?: boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
const transactionDepthByDatabase = new WeakMap<Database, number>();
|
|
10
|
+
|
|
11
|
+
function getTransactionDepth(db: Database): number {
|
|
12
|
+
return transactionDepthByDatabase.get(db) ?? 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function enterTransaction(db: Database): void {
|
|
16
|
+
const currentDepth = getTransactionDepth(db);
|
|
17
|
+
if (currentDepth > 0) {
|
|
18
|
+
throw new Error("Nested transactions are not supported for this database instance");
|
|
19
|
+
}
|
|
20
|
+
transactionDepthByDatabase.set(db, currentDepth + 1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function exitTransaction(db: Database): void {
|
|
24
|
+
const currentDepth = getTransactionDepth(db);
|
|
25
|
+
if (currentDepth <= 1) {
|
|
26
|
+
transactionDepthByDatabase.delete(db);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
transactionDepthByDatabase.set(db, currentDepth - 1);
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
export function withTransaction<T>(db: Database, fn: () => T, options: TransactionOptions = {}): T {
|
|
10
33
|
const maxRetries = options.maxRetries ?? 5;
|
|
11
34
|
const backoffMs = options.backoffMs ?? 100;
|
|
@@ -14,20 +37,25 @@ export function withTransaction<T>(db: Database, fn: () => T, options: Transacti
|
|
|
14
37
|
let attempts = 0;
|
|
15
38
|
while (true) {
|
|
16
39
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
enterTransaction(db);
|
|
41
|
+
try {
|
|
42
|
+
if (useImmediate) {
|
|
43
|
+
db.run("BEGIN IMMEDIATE");
|
|
44
|
+
try {
|
|
45
|
+
const result = fn();
|
|
46
|
+
db.run("COMMIT");
|
|
47
|
+
return result;
|
|
48
|
+
} catch (innerError) {
|
|
49
|
+
db.run("ROLLBACK");
|
|
50
|
+
throw innerError;
|
|
51
|
+
}
|
|
26
52
|
}
|
|
27
|
-
}
|
|
28
53
|
|
|
29
|
-
|
|
30
|
-
|
|
54
|
+
const transaction = db.transaction(fn);
|
|
55
|
+
return transaction();
|
|
56
|
+
} finally {
|
|
57
|
+
exitTransaction(db);
|
|
58
|
+
}
|
|
31
59
|
} catch (error: unknown) {
|
|
32
60
|
const e = error as Error;
|
|
33
61
|
const isBusyError =
|
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
import type { ForensicEventDomain, ForensicEventType } from "../observability/forensic-types";
|
|
6
6
|
import type { LogEntry, LogSink } from "./types";
|
|
7
7
|
|
|
8
|
+
function normalizeTaskId(taskId: unknown): number | null {
|
|
9
|
+
return typeof taskId === "number" && Number.isInteger(taskId) && taskId > 0 ? taskId : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
export function createForensicSinkForArtifactDir(artifactDir: string): LogSink {
|
|
9
13
|
return {
|
|
10
14
|
write(entry: LogEntry): void {
|
|
@@ -63,7 +67,7 @@ export function createForensicSinkForArtifactDir(artifactDir: string): LogSink {
|
|
|
63
67
|
parentSessionId: (parentSessionId as string) ?? null,
|
|
64
68
|
phase: (phase as string) ?? null,
|
|
65
69
|
dispatchId: (dispatchId as string) ?? null,
|
|
66
|
-
taskId: (taskId
|
|
70
|
+
taskId: normalizeTaskId(taskId),
|
|
67
71
|
agent: (agent as string) ?? null,
|
|
68
72
|
type: forensicType,
|
|
69
73
|
code: (code as string) ?? null,
|
|
@@ -136,7 +140,7 @@ export function createForensicSink(projectRoot: string): LogSink {
|
|
|
136
140
|
parentSessionId: (parentSessionId as string) ?? null,
|
|
137
141
|
phase: (phase as string) ?? null,
|
|
138
142
|
dispatchId: (dispatchId as string) ?? null,
|
|
139
|
-
taskId: (taskId
|
|
143
|
+
taskId: normalizeTaskId(taskId),
|
|
140
144
|
agent: (agent as string) ?? null,
|
|
141
145
|
type: forensicType,
|
|
142
146
|
code: (code as string) ?? null,
|
package/src/logging/index.ts
CHANGED
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { McpLifecycleManager } from "./manager";
|
|
2
|
+
export type { McpToolAction, ScopeViolation } from "./scope-filter";
|
|
3
|
+
export { filterByScope, isActionAllowed } from "./scope-filter";
|
|
4
|
+
export type {
|
|
5
|
+
ManagedMcpServer,
|
|
6
|
+
McpHealthResult,
|
|
7
|
+
McpScope,
|
|
8
|
+
McpServerState,
|
|
9
|
+
McpTransport,
|
|
10
|
+
SkillMcpConfig,
|
|
11
|
+
} from "./types";
|
|
12
|
+
export {
|
|
13
|
+
mcpScopeSchema,
|
|
14
|
+
mcpTransportSchema,
|
|
15
|
+
skillMcpConfigSchema,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
let globalMcpManager: InstanceType<typeof import("./manager").McpLifecycleManager> | null = null;
|
|
19
|
+
|
|
20
|
+
export function setGlobalMcpManager(
|
|
21
|
+
manager: InstanceType<typeof import("./manager").McpLifecycleManager>,
|
|
22
|
+
): void {
|
|
23
|
+
globalMcpManager = manager;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getGlobalMcpManager(): InstanceType<
|
|
27
|
+
typeof import("./manager").McpLifecycleManager
|
|
28
|
+
> | null {
|
|
29
|
+
return globalMcpManager;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resetGlobalMcpManager(): void {
|
|
33
|
+
globalMcpManager = null;
|
|
34
|
+
}
|