akm-cli 0.9.0-rc.2 → 0.9.0-rc.4
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/CHANGELOG.md +40 -12
- package/README.md +7 -8
- package/SECURITY.md +5 -3
- package/dist/akm +44 -33
- package/dist/akm-migrate-storage +44 -33
- package/dist/assets/backends/schtasks-template.xml +2 -1
- package/dist/assets/improve-strategies/catchup.json +3 -1
- package/dist/assets/improve-strategies/consolidate.json +3 -1
- package/dist/assets/improve-strategies/frequent.json +3 -1
- package/dist/assets/improve-strategies/graph-refresh.json +3 -1
- package/dist/assets/improve-strategies/memory-focus.json +4 -1
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -0
- package/dist/assets/improve-strategies/quick.json +3 -1
- package/dist/assets/improve-strategies/recombine-only.json +2 -0
- package/dist/assets/improve-strategies/reflect-distill.json +1 -0
- package/dist/assets/improve-strategies/synthesize.json +2 -0
- package/dist/assets/tasks/core/backup.yml +2 -2
- package/dist/cli/config-migrate.js +554 -26
- package/dist/cli.js +28 -11
- package/dist/commands/backup-cli.js +6 -4
- package/dist/commands/config-cli.js +10 -3
- package/dist/commands/feedback-cli.js +24 -7
- package/dist/commands/health/checks.js +94 -15
- package/dist/commands/health/html-report.js +23 -2
- package/dist/commands/health/improve-metrics.js +45 -6
- package/dist/commands/health/md-report.js +10 -1
- package/dist/commands/health/surfaces.js +2 -1
- package/dist/commands/health/windows.js +1 -1
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/anti-collapse.js +3 -3
- package/dist/commands/improve/collapse-detector.js +5 -5
- package/dist/commands/improve/consolidate.js +123 -66
- package/dist/commands/improve/distill/promote-memory.js +8 -6
- package/dist/commands/improve/distill/quality-gate.js +1 -1
- package/dist/commands/improve/distill-guards.js +1 -1
- package/dist/commands/improve/distill-promotion-policy.js +32 -26
- package/dist/commands/improve/distill.js +52 -36
- package/dist/commands/improve/eligibility.js +9 -8
- package/dist/commands/improve/extract-prompt.js +2 -2
- package/dist/commands/improve/extract.js +43 -11
- package/dist/commands/improve/improve-auto-accept.js +14 -28
- package/dist/commands/improve/improve-cli.js +4 -2
- package/dist/commands/improve/improve-strategies.js +2 -1
- package/dist/commands/improve/improve.js +49 -8
- package/dist/commands/improve/loop-stages.js +11 -6
- package/dist/commands/improve/preparation.js +51 -17
- package/dist/commands/improve/procedural.js +10 -6
- package/dist/commands/improve/recombine.js +31 -7
- package/dist/commands/improve/reflect.js +24 -14
- package/dist/commands/improve/salience.js +5 -1
- package/dist/commands/improve/source-identity.js +43 -0
- package/dist/commands/migrate-cli.js +36 -0
- package/dist/commands/mv-cli.js +647 -258
- package/dist/commands/proposal/drain.js +16 -4
- package/dist/commands/proposal/proposal-cli.js +3 -2
- package/dist/commands/proposal/proposal.js +16 -55
- package/dist/commands/proposal/repository.js +664 -58
- package/dist/commands/read/curate.js +4 -2
- package/dist/commands/read/knowledge.js +4 -1
- package/dist/commands/read/search.js +6 -2
- package/dist/commands/read/show.js +8 -7
- package/dist/commands/sources/self-update.js +156 -112
- package/dist/commands/sources/sources-cli.js +7 -2
- package/dist/commands/tasks/default-tasks.js +5 -5
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +261 -68
- package/dist/core/common.js +36 -3
- package/dist/core/config/config-io.js +2 -2
- package/dist/core/config/config-schema.js +15 -3
- package/dist/core/config/config.js +14 -13
- package/dist/core/file-lock.js +2 -2
- package/dist/core/improve-result.js +87 -7
- package/dist/core/migration-backup.js +816 -171
- package/dist/core/migration-operation.js +44 -0
- package/dist/core/state/migrations.js +4 -7
- package/dist/core/state-db.js +23 -7
- package/dist/core/write-source.js +86 -18
- package/dist/indexer/db/db.js +109 -53
- package/dist/indexer/index-writer-lock.js +38 -37
- package/dist/indexer/index-written-assets.js +73 -56
- package/dist/indexer/indexer.js +5 -1
- package/dist/indexer/search/search-source.js +2 -2
- package/dist/indexer/usage/usage-events.js +8 -2
- package/dist/integrations/agent/engine-resolution.js +14 -7
- package/dist/scripts/migrate-storage.js +887 -992
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +494 -574
- package/dist/setup/detected-engines.js +7 -13
- package/dist/setup/engine-config.js +14 -4
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/connection.js +1 -1
- package/dist/setup/steps/tasks.js +1 -1
- package/dist/sources/providers/git-stash.js +58 -21
- package/dist/sources/providers/git.js +1 -1
- package/dist/storage/engines/sqlite-migrations.js +138 -3
- package/dist/storage/repositories/events-repository.js +24 -0
- package/dist/storage/repositories/proposals-repository.js +14 -0
- package/dist/storage/repositories/task-history-repository.js +30 -3
- package/dist/tasks/backends/cron.js +71 -38
- package/dist/tasks/backends/exec-utils.js +55 -3
- package/dist/tasks/backends/launchd.js +241 -50
- package/dist/tasks/backends/schtasks.js +434 -59
- package/dist/tasks/command-executable.js +93 -0
- package/dist/tasks/embedded.js +2 -0
- package/dist/tasks/parser.js +142 -6
- package/dist/tasks/resolve-akm-bin.js +19 -4
- package/dist/tasks/runner.js +258 -93
- package/dist/tasks/schedule.js +108 -19
- package/dist/tasks/scheduler-invocation.js +86 -0
- package/dist/tasks/task-id.js +37 -0
- package/dist/workflows/db.js +24 -13
- package/dist/workflows/validator.js +24 -2
- package/docs/README.md +103 -0
- package/docs/migration/release-notes/0.9.0.md +28 -9
- package/docs/migration/v0.8-to-v0.9.md +186 -26
- package/package.json +2 -3
package/dist/tasks/runner.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* `tasks sync` time, not here — at run time we still want to attempt
|
|
13
13
|
* execution and surface the actual failure rather than re-fail on a
|
|
14
14
|
* validation error that the user already knows about.)
|
|
15
|
-
* 3.
|
|
15
|
+
* 3. Skip disabled tasks only when the invocation is scheduler-generated;
|
|
16
|
+
* explicit manual runs are allowed for catch-up and testing.
|
|
16
17
|
* 4. Dispatch by target kind:
|
|
17
18
|
* • workflow → `startWorkflowRun(ref, params)`
|
|
18
19
|
* • prompt → `executeRunner(engine, prompt, { stdio: "captured" })`
|
|
@@ -25,97 +26,135 @@
|
|
|
25
26
|
* and so tests can assert against it without scraping stdout.
|
|
26
27
|
*/
|
|
27
28
|
import fs from "node:fs";
|
|
29
|
+
import os from "node:os";
|
|
28
30
|
import path from "node:path";
|
|
29
31
|
import { assertNever } from "../core/assert.js";
|
|
30
32
|
import { parseAssetRef } from "../core/asset/asset-ref.js";
|
|
31
33
|
import { resolveStashDir } from "../core/common.js";
|
|
32
34
|
import { loadConfig } from "../core/config/config.js";
|
|
33
|
-
import { NotFoundError, rethrowIfTestIsolationError } from "../core/errors.js";
|
|
35
|
+
import { AkmError, NotFoundError, rethrowIfTestIsolationError } from "../core/errors.js";
|
|
34
36
|
import { buildTaskRunId, insertTaskLogLines, openLogsDatabase, } from "../core/logs-db.js";
|
|
35
37
|
import { getTaskLogDir } from "../core/paths.js";
|
|
36
38
|
import { withStateDb } from "../core/state-db.js";
|
|
37
|
-
import { error } from "../core/warn.js";
|
|
38
39
|
import { resolveEngine, resolveLlmEngineUse } from "../integrations/agent/engine-resolution.js";
|
|
39
40
|
import { resolveModel } from "../integrations/agent/model-aliases.js";
|
|
40
41
|
import { executeRunner } from "../integrations/agent/runner-dispatch.js";
|
|
41
42
|
import { chatCompletion } from "../llm/client.js";
|
|
42
43
|
import { spawn } from "../runtime.js";
|
|
43
44
|
import { resolveAssetPath } from "../sources/resolve.js";
|
|
44
|
-
import { decodeTaskHistoryMetadata, getTaskHistory, queryTaskHistory, upsertTaskHistory, } from "../storage/repositories/task-history-repository.js";
|
|
45
|
+
import { decodeTaskHistoryMetadata, finalizeTaskHistoryAttempt, getTaskHistory, queryTaskHistory, reserveTaskHistoryAttempt, upsertTaskHistory, } from "../storage/repositories/task-history-repository.js";
|
|
45
46
|
import { startWorkflowRun } from "../workflows/runtime/runs.js";
|
|
47
|
+
import { findBareAkmExecutableIndex } from "./command-executable.js";
|
|
46
48
|
import { parseTaskDocument } from "./parser.js";
|
|
49
|
+
import { resolveAkmInvocation } from "./resolve-akm-bin.js";
|
|
50
|
+
import { validateTaskId } from "./task-id.js";
|
|
51
|
+
export const INVALID_TASK_ATTEMPT_ID = "_invalid-task-id";
|
|
47
52
|
export async function runTask(id, options = {}) {
|
|
48
|
-
const stashDir = options.stashDir ?? resolveStashDir();
|
|
49
53
|
const runAgentImpl = options.runAgentImpl;
|
|
50
54
|
const startWorkflowRunImpl = options.startWorkflowRunImpl ?? startWorkflowRun;
|
|
51
55
|
const now = options.now ?? (() => new Date());
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
: task.target.kind === "command"
|
|
67
|
-
? { kind: "command", cmd: task.target.cmd }
|
|
68
|
-
: { kind: "prompt", engine: task.target.engine ?? null };
|
|
69
|
-
const result = {
|
|
70
|
-
id,
|
|
71
|
-
status: "disabled",
|
|
72
|
-
startedAt: startedIso,
|
|
73
|
-
finishedAt: finishedAt.toISOString(),
|
|
74
|
-
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
75
|
-
log: logPath,
|
|
76
|
-
target: disabledTarget,
|
|
77
|
-
};
|
|
78
|
-
const disabledLine = `[akm tasks] task "${id}" is disabled — skipping run.`;
|
|
79
|
-
persistRunLog({
|
|
80
|
-
taskId: id,
|
|
81
|
-
startedAtIso: startedIso,
|
|
82
|
-
finishedAtIso: result.finishedAt,
|
|
83
|
-
logPath,
|
|
84
|
-
fileText: `${disabledLine}\n`,
|
|
85
|
-
dbLines: [{ line: disabledLine }],
|
|
56
|
+
const requestedStartedAt = now();
|
|
57
|
+
try {
|
|
58
|
+
validateTaskId(id);
|
|
59
|
+
}
|
|
60
|
+
catch (failure) {
|
|
61
|
+
const attempt = reserveTaskAttempt(INVALID_TASK_ATTEMPT_ID, requestedStartedAt);
|
|
62
|
+
recordTaskAttemptFailure({
|
|
63
|
+
taskId: INVALID_TASK_ATTEMPT_ID,
|
|
64
|
+
reason: "invalid_task_id",
|
|
65
|
+
failure,
|
|
66
|
+
startedAt: attempt.startedAt,
|
|
67
|
+
finishedAt: now(),
|
|
68
|
+
logDir: options.logDir,
|
|
69
|
+
historyReserved: attempt.historyReserved,
|
|
86
70
|
});
|
|
87
|
-
|
|
88
|
-
return result;
|
|
71
|
+
throw failure;
|
|
89
72
|
}
|
|
90
|
-
|
|
91
|
-
|
|
73
|
+
const attempt = reserveTaskAttempt(id, requestedStartedAt);
|
|
74
|
+
const startedAt = attempt.startedAt;
|
|
75
|
+
let failureReason = "task_load_failed";
|
|
76
|
+
try {
|
|
77
|
+
const stashDir = options.stashDir ?? resolveStashDir();
|
|
78
|
+
const filePath = await resolveAssetPath(stashDir, "task", id);
|
|
79
|
+
const yaml = fs.readFileSync(filePath, "utf8");
|
|
80
|
+
failureReason = "task_parse_failed";
|
|
81
|
+
const task = parseTaskDocument({ yaml, filePath, id });
|
|
82
|
+
failureReason = "task_dispatch_failed";
|
|
83
|
+
const startedIso = startedAt.toISOString();
|
|
84
|
+
const logPath = resolveTaskLogPath(options.logDir, id, startedIso);
|
|
85
|
+
if (!task.enabled && options.scheduled === true) {
|
|
86
|
+
const finishedAt = finishAttempt(startedAt, now());
|
|
87
|
+
const disabledTarget = task.target.kind === "workflow"
|
|
88
|
+
? { kind: "workflow", ref: task.target.ref }
|
|
89
|
+
: task.target.kind === "command"
|
|
90
|
+
? { kind: "command", cmd: task.target.cmd }
|
|
91
|
+
: { kind: "prompt", engine: task.target.engine ?? null };
|
|
92
|
+
const result = {
|
|
93
|
+
id,
|
|
94
|
+
status: "disabled",
|
|
95
|
+
startedAt: startedIso,
|
|
96
|
+
finishedAt: finishedAt.toISOString(),
|
|
97
|
+
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
98
|
+
log: logPath,
|
|
99
|
+
target: disabledTarget,
|
|
100
|
+
};
|
|
101
|
+
const disabledLine = `[akm tasks] task "${id}" is disabled — skipping run.`;
|
|
102
|
+
persistRunLog({
|
|
103
|
+
taskId: id,
|
|
104
|
+
startedAtIso: startedIso,
|
|
105
|
+
finishedAtIso: result.finishedAt,
|
|
106
|
+
logPath,
|
|
107
|
+
fileText: `${disabledLine}\n`,
|
|
108
|
+
dbLines: [{ line: disabledLine }],
|
|
109
|
+
});
|
|
110
|
+
appendHistory(result, attempt.historyReserved);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
if (task.target.kind === "workflow") {
|
|
114
|
+
return await runWorkflowTask({
|
|
115
|
+
task,
|
|
116
|
+
logPath,
|
|
117
|
+
startedAt,
|
|
118
|
+
now,
|
|
119
|
+
startWorkflowRunImpl,
|
|
120
|
+
historyReserved: attempt.historyReserved,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (task.target.kind === "command") {
|
|
124
|
+
return await runCommandTask({ task, logPath, startedAt, now, historyReserved: attempt.historyReserved });
|
|
125
|
+
}
|
|
126
|
+
return await runPromptTask({
|
|
92
127
|
task,
|
|
128
|
+
stashDir,
|
|
93
129
|
logPath,
|
|
94
130
|
startedAt,
|
|
95
131
|
now,
|
|
96
|
-
|
|
132
|
+
runAgentImpl,
|
|
133
|
+
agentOptions: options.agentOptions,
|
|
134
|
+
chatCompletionImpl: options.chatCompletionImpl ?? chatCompletion,
|
|
135
|
+
historyReserved: attempt.historyReserved,
|
|
97
136
|
});
|
|
98
137
|
}
|
|
99
|
-
|
|
100
|
-
|
|
138
|
+
catch (failure) {
|
|
139
|
+
recordTaskAttemptFailure({
|
|
140
|
+
taskId: id,
|
|
141
|
+
reason: failureReason,
|
|
142
|
+
failure,
|
|
143
|
+
startedAt,
|
|
144
|
+
finishedAt: now(),
|
|
145
|
+
logDir: options.logDir,
|
|
146
|
+
historyReserved: attempt.historyReserved,
|
|
147
|
+
});
|
|
148
|
+
throw failure;
|
|
101
149
|
}
|
|
102
|
-
return await runPromptTask({
|
|
103
|
-
task,
|
|
104
|
-
stashDir,
|
|
105
|
-
logPath,
|
|
106
|
-
startedAt,
|
|
107
|
-
now,
|
|
108
|
-
runAgentImpl,
|
|
109
|
-
agentOptions: options.agentOptions,
|
|
110
|
-
chatCompletionImpl: options.chatCompletionImpl ?? chatCompletion,
|
|
111
|
-
});
|
|
112
150
|
}
|
|
113
151
|
// ── command target ──────────────────────────────────────────────────────────
|
|
114
152
|
async function runCommandTask(input) {
|
|
115
|
-
const { task, logPath, startedAt, now } = input;
|
|
153
|
+
const { task, logPath, startedAt, now, historyReserved } = input;
|
|
116
154
|
if (task.target.kind !== "command")
|
|
117
155
|
throw new Error("invariant: command target");
|
|
118
156
|
const { cmd } = task.target;
|
|
157
|
+
const spawnCmd = resolveNestedAkmCommand(cmd);
|
|
119
158
|
const timeoutMs = task.timeoutMs !== undefined ? task.timeoutMs : null;
|
|
120
159
|
const header = `[akm tasks] task=${task.id} kind=command cmd=${cmd.join(" ")}`;
|
|
121
160
|
const logLines = [header];
|
|
@@ -124,11 +163,11 @@ async function runCommandTask(input) {
|
|
|
124
163
|
let stderr = "";
|
|
125
164
|
let exitCode = null;
|
|
126
165
|
try {
|
|
127
|
-
const proc = spawn(
|
|
166
|
+
const proc = spawn(spawnCmd, {
|
|
128
167
|
stdin: "ignore",
|
|
129
168
|
stdout: "pipe",
|
|
130
169
|
stderr: "pipe",
|
|
131
|
-
cwd: process.env.HOME ??
|
|
170
|
+
cwd: process.env.HOME ?? os.tmpdir(),
|
|
132
171
|
// Stamp task-runner provenance so any akm invocation in the command tree
|
|
133
172
|
// records usage events as machine traffic, not user demand (DRIFT-6).
|
|
134
173
|
// A more specific stamp already in the environment (e.g. improve's
|
|
@@ -181,7 +220,7 @@ async function runCommandTask(input) {
|
|
|
181
220
|
dbLines.push({ level: "error", line: `spawn_error=${msg}` });
|
|
182
221
|
exitCode = 1;
|
|
183
222
|
}
|
|
184
|
-
const finishedAt = now();
|
|
223
|
+
const finishedAt = finishAttempt(startedAt, now());
|
|
185
224
|
persistRunLog({
|
|
186
225
|
taskId: task.id,
|
|
187
226
|
startedAtIso: startedAt.toISOString(),
|
|
@@ -201,12 +240,19 @@ async function runCommandTask(input) {
|
|
|
201
240
|
target: { kind: "command", cmd },
|
|
202
241
|
detail: { exitCode },
|
|
203
242
|
};
|
|
204
|
-
appendHistory(result);
|
|
243
|
+
appendHistory(result, historyReserved);
|
|
205
244
|
return result;
|
|
206
245
|
}
|
|
246
|
+
/** Avoid a second PATH lookup when a task invokes the same AKM installation. */
|
|
247
|
+
function resolveNestedAkmCommand(cmd) {
|
|
248
|
+
const akmIndex = findBareAkmExecutableIndex(cmd);
|
|
249
|
+
if (akmIndex === undefined)
|
|
250
|
+
return cmd;
|
|
251
|
+
return [...cmd.slice(0, akmIndex), ...resolveAkmInvocation().argv, ...cmd.slice(akmIndex + 1)];
|
|
252
|
+
}
|
|
207
253
|
// ── workflow target ─────────────────────────────────────────────────────────
|
|
208
254
|
async function runWorkflowTask(input) {
|
|
209
|
-
const { task, logPath, startedAt, now, startWorkflowRunImpl } = input;
|
|
255
|
+
const { task, logPath, startedAt, now, startWorkflowRunImpl, historyReserved } = input;
|
|
210
256
|
if (task.target.kind !== "workflow")
|
|
211
257
|
throw new Error("invariant: workflow target");
|
|
212
258
|
const ref = parseAssetRef(task.target.ref);
|
|
@@ -219,9 +265,11 @@ async function runWorkflowTask(input) {
|
|
|
219
265
|
detail = await startWorkflowRunImpl(task.target.ref, task.target.params);
|
|
220
266
|
}
|
|
221
267
|
catch (e) {
|
|
268
|
+
if (e instanceof AkmError && e.kind === "config")
|
|
269
|
+
throw e;
|
|
222
270
|
error = e instanceof Error ? e : new Error(String(e));
|
|
223
271
|
}
|
|
224
|
-
const finishedAt = now();
|
|
272
|
+
const finishedAt = finishAttempt(startedAt, now());
|
|
225
273
|
const status = error ? "failed" : mapWorkflowStatus(detail?.run.status);
|
|
226
274
|
const log = renderWorkflowLog({ task, detail, error });
|
|
227
275
|
persistRunLog({
|
|
@@ -245,7 +293,7 @@ async function runWorkflowTask(input) {
|
|
|
245
293
|
...(error ? { error: error.message } : {}),
|
|
246
294
|
},
|
|
247
295
|
};
|
|
248
|
-
appendHistory(result);
|
|
296
|
+
appendHistory(result, historyReserved);
|
|
249
297
|
// Don't re-throw on workflow failure: the OS scheduler reads exit codes,
|
|
250
298
|
// not exceptions, and the CLI maps `status: "failed"` to a non-zero exit
|
|
251
299
|
// via exitCodeForStatus(). Throwing here would route through the generic
|
|
@@ -359,7 +407,7 @@ async function runPromptTask(input) {
|
|
|
359
407
|
return { ok: true, exitCode: 0, stdout, stderr: "", durationMs: Date.now() - started };
|
|
360
408
|
},
|
|
361
409
|
});
|
|
362
|
-
const finishedAt = now();
|
|
410
|
+
const finishedAt = finishAttempt(startedAt, now());
|
|
363
411
|
const log = renderPromptLog({ task, engineName, result });
|
|
364
412
|
persistRunLog({
|
|
365
413
|
taskId: task.id,
|
|
@@ -382,7 +430,7 @@ async function runPromptTask(input) {
|
|
|
382
430
|
? { exitCode: result.exitCode }
|
|
383
431
|
: { reason: result.reason, error: result.error, exitCode: result.exitCode },
|
|
384
432
|
};
|
|
385
|
-
appendHistory(out);
|
|
433
|
+
appendHistory(out, input.historyReserved);
|
|
386
434
|
return out;
|
|
387
435
|
}
|
|
388
436
|
async function resolvePromptText(task, stashDir) {
|
|
@@ -428,6 +476,19 @@ function renderPromptLog(input) {
|
|
|
428
476
|
}
|
|
429
477
|
return { fileText: `${lines.join("\n")}\n`, dbLines };
|
|
430
478
|
}
|
|
479
|
+
function taskLogPath(logDir, taskId, startedAtIso) {
|
|
480
|
+
const tsSlug = startedAtIso.replace(/[:.]/g, "-");
|
|
481
|
+
return path.join(logDir, taskId, `${tsSlug}.log`);
|
|
482
|
+
}
|
|
483
|
+
function resolveTaskLogPath(logDir, taskId, startedAtIso) {
|
|
484
|
+
try {
|
|
485
|
+
return taskLogPath(logDir ?? getTaskLogDir(), taskId, startedAtIso);
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
rethrowIfTestIsolationError(error);
|
|
489
|
+
return "";
|
|
490
|
+
}
|
|
491
|
+
}
|
|
431
492
|
/** Split captured pipe output into per-line logs.db rows (blank lines dropped). */
|
|
432
493
|
function streamLines(text, stream, level) {
|
|
433
494
|
return text
|
|
@@ -444,7 +505,16 @@ function streamLines(text, stream, level) {
|
|
|
444
505
|
* logs.db must never fail a task run.
|
|
445
506
|
*/
|
|
446
507
|
function persistRunLog(input) {
|
|
447
|
-
|
|
508
|
+
if (input.logPath) {
|
|
509
|
+
try {
|
|
510
|
+
fs.mkdirSync(path.dirname(input.logPath), { recursive: true });
|
|
511
|
+
fs.writeFileSync(input.logPath, input.fileText);
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
rethrowIfTestIsolationError(error);
|
|
515
|
+
// Transitional file logging is fully best-effort.
|
|
516
|
+
}
|
|
517
|
+
}
|
|
448
518
|
try {
|
|
449
519
|
const db = openLogsDatabase();
|
|
450
520
|
try {
|
|
@@ -459,36 +529,129 @@ function persistRunLog(input) {
|
|
|
459
529
|
db.close();
|
|
460
530
|
}
|
|
461
531
|
}
|
|
462
|
-
catch (
|
|
463
|
-
rethrowIfTestIsolationError(
|
|
464
|
-
|
|
532
|
+
catch (error) {
|
|
533
|
+
rethrowIfTestIsolationError(error);
|
|
534
|
+
// Structured logging is fully best-effort and must not alter CLI output.
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/** Reserve a collision-free identity through state.db's existing unique index. */
|
|
538
|
+
function reserveTaskAttempt(taskId, requestedStartedAt) {
|
|
539
|
+
try {
|
|
540
|
+
return withStateDb((db) => {
|
|
541
|
+
for (let offsetMs = 0;; offsetMs++) {
|
|
542
|
+
const startedAt = new Date(requestedStartedAt.getTime() + offsetMs);
|
|
543
|
+
const reserved = reserveTaskHistoryAttempt(db, {
|
|
544
|
+
task_id: taskId,
|
|
545
|
+
status: "active",
|
|
546
|
+
started_at: startedAt.toISOString(),
|
|
547
|
+
completed_at: null,
|
|
548
|
+
failed_at: null,
|
|
549
|
+
log_path: null,
|
|
550
|
+
target_kind: null,
|
|
551
|
+
target_ref: null,
|
|
552
|
+
metadata_json: JSON.stringify({ metadataVersion: 2, durationMs: 0, detail: null }),
|
|
553
|
+
});
|
|
554
|
+
if (reserved)
|
|
555
|
+
return { startedAt, historyReserved: true };
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
rethrowIfTestIsolationError(error);
|
|
561
|
+
// Attempt recording cannot prevent or replace task execution.
|
|
562
|
+
return { startedAt: requestedStartedAt, historyReserved: false };
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function finishAttempt(startedAt, observedFinishedAt) {
|
|
566
|
+
return observedFinishedAt.getTime() < startedAt.getTime() ? new Date(startedAt) : observedFinishedAt;
|
|
567
|
+
}
|
|
568
|
+
const SAFE_TASK_ATTEMPT_ERROR_CODES = new Set([
|
|
569
|
+
"CONFIG_DIR_UNRESOLVABLE",
|
|
570
|
+
"STASH_DIR_NOT_FOUND",
|
|
571
|
+
"STASH_DIR_NOT_A_DIRECTORY",
|
|
572
|
+
"STASH_DIR_UNREADABLE",
|
|
573
|
+
"LLM_NOT_CONFIGURED",
|
|
574
|
+
"INVALID_CONFIG_FILE",
|
|
575
|
+
"UNSUPPORTED_CONFIG_VERSION",
|
|
576
|
+
"TEST_ISOLATION_MISSING",
|
|
577
|
+
"INVALID_FLAG_VALUE",
|
|
578
|
+
"MISSING_REQUIRED_ARGUMENT",
|
|
579
|
+
"PATH_ESCAPE_VIOLATION",
|
|
580
|
+
"TASK_SCHEMA_VERSION_UNSUPPORTED",
|
|
581
|
+
"ASSET_NOT_FOUND",
|
|
582
|
+
"WORKFLOW_NOT_FOUND",
|
|
583
|
+
"FILE_NOT_FOUND",
|
|
584
|
+
]);
|
|
585
|
+
function safeTaskAttemptErrorCode(failure) {
|
|
586
|
+
if (failure instanceof AkmError && SAFE_TASK_ATTEMPT_ERROR_CODES.has(failure.code))
|
|
587
|
+
return failure.code;
|
|
588
|
+
return "INTERNAL";
|
|
589
|
+
}
|
|
590
|
+
export function recordTaskAttemptFailure(input) {
|
|
591
|
+
let taskId = input.taskId;
|
|
592
|
+
try {
|
|
593
|
+
validateTaskId(taskId);
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
taskId = INVALID_TASK_ATTEMPT_ID;
|
|
465
597
|
}
|
|
598
|
+
const attempt = input.historyReserved === undefined
|
|
599
|
+
? reserveTaskAttempt(taskId, input.startedAt)
|
|
600
|
+
: { startedAt: input.startedAt, historyReserved: input.historyReserved };
|
|
601
|
+
const finishedAt = finishAttempt(attempt.startedAt, input.finishedAt ?? new Date());
|
|
602
|
+
const startedAtIso = attempt.startedAt.toISOString();
|
|
603
|
+
const finishedAtIso = finishedAt.toISOString();
|
|
604
|
+
const errorCode = safeTaskAttemptErrorCode(input.failure);
|
|
605
|
+
const logPath = resolveTaskLogPath(input.logDir, taskId, startedAtIso);
|
|
606
|
+
const line = `[akm tasks] status=failed reason=${input.reason} code=${errorCode}`;
|
|
607
|
+
const result = {
|
|
608
|
+
id: taskId,
|
|
609
|
+
status: "failed",
|
|
610
|
+
startedAt: startedAtIso,
|
|
611
|
+
finishedAt: finishedAtIso,
|
|
612
|
+
durationMs: Math.max(0, finishedAt.getTime() - attempt.startedAt.getTime()),
|
|
613
|
+
log: logPath,
|
|
614
|
+
target: { kind: "unknown" },
|
|
615
|
+
detail: { reason: input.reason, error: errorCode },
|
|
616
|
+
};
|
|
617
|
+
persistRunLog({
|
|
618
|
+
taskId,
|
|
619
|
+
startedAtIso,
|
|
620
|
+
finishedAtIso,
|
|
621
|
+
logPath,
|
|
622
|
+
fileText: `${line}\n`,
|
|
623
|
+
dbLines: [{ level: "error", line }],
|
|
624
|
+
});
|
|
625
|
+
appendHistory(result, attempt.historyReserved);
|
|
466
626
|
}
|
|
467
627
|
// ── history ─────────────────────────────────────────────────────────────────
|
|
468
|
-
function appendHistory(result) {
|
|
628
|
+
function appendHistory(result, historyReserved = false) {
|
|
629
|
+
const row = {
|
|
630
|
+
task_id: result.id,
|
|
631
|
+
status: result.status,
|
|
632
|
+
started_at: result.startedAt,
|
|
633
|
+
completed_at: result.finishedAt,
|
|
634
|
+
failed_at: result.status === "failed" ? result.finishedAt : null,
|
|
635
|
+
log_path: result.log || null,
|
|
636
|
+
target_kind: result.target.kind === "unknown" ? null : result.target.kind,
|
|
637
|
+
target_ref: result.target.kind === "workflow" ? result.target.ref : null,
|
|
638
|
+
metadata_json: JSON.stringify({
|
|
639
|
+
metadataVersion: 2,
|
|
640
|
+
durationMs: result.durationMs,
|
|
641
|
+
detail: result.detail ?? null,
|
|
642
|
+
...(result.target.kind === "prompt" ? { engine: result.target.engine } : {}),
|
|
643
|
+
}),
|
|
644
|
+
};
|
|
469
645
|
try {
|
|
470
646
|
withStateDb((db) => {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
started_at: result.startedAt,
|
|
475
|
-
completed_at: result.finishedAt,
|
|
476
|
-
failed_at: result.status === "failed" ? result.finishedAt : null,
|
|
477
|
-
log_path: result.log,
|
|
478
|
-
target_kind: result.target.kind,
|
|
479
|
-
target_ref: result.target.kind === "workflow" ? result.target.ref : null,
|
|
480
|
-
metadata_json: JSON.stringify({
|
|
481
|
-
metadataVersion: 2,
|
|
482
|
-
durationMs: result.durationMs,
|
|
483
|
-
detail: result.detail ?? null,
|
|
484
|
-
...(result.target.kind === "prompt" ? { engine: result.target.engine } : {}),
|
|
485
|
-
}),
|
|
486
|
-
});
|
|
647
|
+
if (historyReserved && finalizeTaskHistoryAttempt(db, row))
|
|
648
|
+
return;
|
|
649
|
+
upsertTaskHistory(db, row);
|
|
487
650
|
});
|
|
488
651
|
}
|
|
489
|
-
catch (
|
|
490
|
-
rethrowIfTestIsolationError(
|
|
491
|
-
|
|
652
|
+
catch (error) {
|
|
653
|
+
rethrowIfTestIsolationError(error);
|
|
654
|
+
// History recording is fully best-effort and must not alter CLI output.
|
|
492
655
|
}
|
|
493
656
|
}
|
|
494
657
|
export function readTaskHistory(options = {}) {
|
|
@@ -518,9 +681,11 @@ function taskHistoryRowToResult(row) {
|
|
|
518
681
|
? { kind: "workflow", ref: row.target_ref ?? "" }
|
|
519
682
|
: row.target_kind === "command"
|
|
520
683
|
? { kind: "command" }
|
|
521
|
-
:
|
|
522
|
-
?
|
|
523
|
-
|
|
684
|
+
: row.target_kind === "prompt"
|
|
685
|
+
? meta.metadataVersion === 2
|
|
686
|
+
? { kind: "prompt", engine: meta.engine ?? null }
|
|
687
|
+
: { kind: "prompt", engine: null, ...(meta.legacyProfile ? { legacyProfile: meta.legacyProfile } : {}) }
|
|
688
|
+
: { kind: "unknown" };
|
|
524
689
|
return {
|
|
525
690
|
id: row.task_id,
|
|
526
691
|
status: row.status,
|