open-multi-agent-kit 0.91.0 → 0.93.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/CHANGELOG.md +32 -1
- package/README.md +2 -2
- package/dist/core/agent-session.d.ts +29 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +89 -21
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/command-safety.d.ts.map +1 -1
- package/dist/core/command-safety.js +0 -50
- package/dist/core/command-safety.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +5 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +5 -7
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +12 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +28 -15
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/transaction.d.ts.map +1 -1
- package/dist/core/compaction/transaction.js +7 -3
- package/dist/core/compaction/transaction.js.map +1 -1
- package/dist/core/context-budget-headroom-candidates.d.ts.map +1 -1
- package/dist/core/context-budget-headroom-candidates.js +21 -11
- package/dist/core/context-budget-headroom-candidates.js.map +1 -1
- package/dist/core/context-budget-system-prompt-items.d.ts +1 -0
- package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
- package/dist/core/context-budget-system-prompt-items.js +7 -8
- package/dist/core/context-budget-system-prompt-items.js.map +1 -1
- package/dist/core/extensions/builtin/command-safety-gate.d.ts +8 -0
- package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
- package/dist/core/extensions/builtin/command-safety-gate.js +23 -0
- package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +4 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/keybindings.d.ts +5 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +5 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/loadouts.d.ts.map +1 -1
- package/dist/core/loadouts.js +27 -48
- package/dist/core/loadouts.js.map +1 -1
- package/dist/core/model-resolver.d.ts +41 -3
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +17 -9
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/provider-resilience.d.ts.map +1 -1
- package/dist/core/provider-resilience.js +3 -1
- package/dist/core/provider-resilience.js.map +1 -1
- package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4-weights.js +9 -3
- package/dist/core/reasoning-router-v4-weights.js.map +1 -1
- package/dist/core/reasoning-router-v4.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4.js +94 -24
- package/dist/core/reasoning-router-v4.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +11 -5
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/session-file-compaction.d.ts.map +1 -1
- package/dist/core/session-file-compaction.js +39 -5
- package/dist/core/session-file-compaction.js.map +1 -1
- package/dist/core/session-termination.d.ts.map +1 -1
- package/dist/core/session-termination.js +1 -1
- package/dist/core/session-termination.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +8 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +36 -31
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +2 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +38 -11
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +112 -11
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/control-panel-box.d.ts +27 -0
- package/dist/modes/interactive/components/control-panel-box.d.ts.map +1 -1
- package/dist/modes/interactive/components/control-panel-box.js +60 -8
- package/dist/modes/interactive/components/control-panel-box.js.map +1 -1
- package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
- package/dist/modes/interactive/components/control-panel-layout.js +100 -45
- package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
- package/dist/modes/interactive/components/control-panel-runtime-status.d.ts +9 -0
- package/dist/modes/interactive/components/control-panel-runtime-status.d.ts.map +1 -1
- package/dist/modes/interactive/components/control-panel-runtime-status.js +10 -8
- package/dist/modes/interactive/components/control-panel-runtime-status.js.map +1 -1
- package/dist/modes/interactive/components/control-panel-sparkles.d.ts +5 -0
- package/dist/modes/interactive/components/control-panel-sparkles.d.ts.map +1 -1
- package/dist/modes/interactive/components/control-panel-sparkles.js +14 -9
- package/dist/modes/interactive/components/control-panel-sparkles.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +10 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +3 -3
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.d.ts +49 -0
- package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -0
- package/dist/modes/interactive/components/status-sidebar.js +253 -0
- package/dist/modes/interactive/components/status-sidebar.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +9 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +94 -20
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/omk-neon-control.json +13 -13
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +9 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +19 -8
- package/dist/utils/clipboard.js.map +1 -1
- package/docs/adaptorch-preview-spec.md +174 -0
- package/docs/adaptorch-preview.md +1 -1
- package/docs/compaction.md +20 -13
- package/docs/correctness-wall.md +13 -5
- package/docs/custom-provider.md +8 -8
- package/docs/development.md +2 -2
- package/docs/docs.json +4 -0
- package/docs/environment-variables.md +90 -0
- package/docs/extensions.md +8 -8
- package/docs/index.md +1 -0
- package/docs/json.md +4 -4
- package/docs/providers.md +1 -1
- package/docs/rpc.md +5 -2
- package/docs/session-format.md +5 -5
- package/docs/settings.md +1 -0
- package/docs/skills.md +2 -0
- package/docs/themes.md +2 -2
- package/docs/tui.md +4 -4
- package/docs/usage.md +4 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/README.md +35 -10
- package/examples/extensions/subagent/adaptive-agent-runtime.test.ts +234 -0
- package/examples/extensions/subagent/adaptive-agent-runtime.ts +240 -0
- package/examples/extensions/subagent/adaptive-result.ts +112 -0
- package/examples/extensions/subagent/agents.test.ts +86 -0
- package/examples/extensions/subagent/agents.ts +18 -4
- package/examples/extensions/subagent/capabilities.ts +15 -1
- package/examples/extensions/subagent/checkpoint-runtime.test.ts +125 -0
- package/examples/extensions/subagent/checkpoint-runtime.ts +198 -0
- package/examples/extensions/subagent/deadline-budget.test.ts +135 -0
- package/examples/extensions/subagent/deadline-budget.ts +242 -0
- package/examples/extensions/subagent/deadline-profile-store.test.ts +72 -0
- package/examples/extensions/subagent/deadline-profile-store.ts +172 -0
- package/examples/extensions/subagent/domain-profiles.ts +272 -0
- package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +13 -0
- package/examples/extensions/subagent/fixtures/hanging-process-tree.mjs +40 -0
- package/examples/extensions/subagent/index.ts +253 -137
- package/examples/extensions/subagent/managed-process.test.ts +178 -0
- package/examples/extensions/subagent/managed-process.ts +236 -0
- package/examples/extensions/subagent/subagent-extension-smoke.test.ts +112 -0
- package/examples/extensions/subagent/subagent-runtime-types.ts +76 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +21 -21
- package/package.json +10 -7
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface CheckpointWorkspace {
|
|
5
|
+
readonly dir: string;
|
|
6
|
+
readonly filePath: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AgentCheckpoint {
|
|
10
|
+
readonly version: 1;
|
|
11
|
+
readonly completed: readonly string[];
|
|
12
|
+
readonly remaining: readonly string[];
|
|
13
|
+
readonly summary: string;
|
|
14
|
+
readonly artifacts: readonly string[];
|
|
15
|
+
readonly messageCount: number;
|
|
16
|
+
readonly outputTail: string;
|
|
17
|
+
readonly lastEventAtMs: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CheckpointEvidence {
|
|
21
|
+
readonly messageCount: number;
|
|
22
|
+
readonly outputTail: string;
|
|
23
|
+
readonly lastEventAtMs: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface BuildCheckpointTaskOptions {
|
|
27
|
+
readonly originalTask: string;
|
|
28
|
+
readonly shardTask: string;
|
|
29
|
+
readonly shardId: string;
|
|
30
|
+
readonly shardIndex: number;
|
|
31
|
+
readonly shardCount: number;
|
|
32
|
+
readonly attempt: number;
|
|
33
|
+
readonly cutoffMs: number;
|
|
34
|
+
readonly checkpointFilePath: string;
|
|
35
|
+
readonly checkpoint?: AgentCheckpoint;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const CHECKPOINT_FILE_LIMIT = 32 * 1024;
|
|
39
|
+
const MAX_LIST_ITEMS = 24;
|
|
40
|
+
const MAX_ITEM_CHARS = 512;
|
|
41
|
+
|
|
42
|
+
export async function createCheckpointWorkspace(prefix: string): Promise<CheckpointWorkspace> {
|
|
43
|
+
const dir = await fs.promises.mkdtemp(prefix);
|
|
44
|
+
const filePath = path.join(dir, "checkpoint.json");
|
|
45
|
+
await writeCheckpoint(filePath, createEmptyCheckpoint());
|
|
46
|
+
return { dir, filePath };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function removeCheckpointWorkspace(workspace: CheckpointWorkspace): Promise<void> {
|
|
50
|
+
await fs.promises.rm(workspace.dir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function writeCheckpoint(filePath: string, checkpoint: AgentCheckpoint): Promise<void> {
|
|
54
|
+
const serialized = JSON.stringify({
|
|
55
|
+
version: 1,
|
|
56
|
+
completed: checkpoint.completed,
|
|
57
|
+
remaining: checkpoint.remaining,
|
|
58
|
+
summary: checkpoint.summary,
|
|
59
|
+
artifacts: checkpoint.artifacts,
|
|
60
|
+
});
|
|
61
|
+
await fs.promises.writeFile(filePath, serialized, { encoding: "utf8", mode: 0o600 });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function readCheckpoint(filePath: string, evidence: CheckpointEvidence): Promise<AgentCheckpoint> {
|
|
65
|
+
let parsed: unknown;
|
|
66
|
+
try {
|
|
67
|
+
const pathStats = await fs.promises.lstat(filePath);
|
|
68
|
+
if (!pathStats.isFile() || pathStats.isSymbolicLink()) return withEvidence(createEmptyCheckpoint(), evidence);
|
|
69
|
+
const handle = await fs.promises.open(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
70
|
+
try {
|
|
71
|
+
const stats = await handle.stat();
|
|
72
|
+
if (stats.size > CHECKPOINT_FILE_LIMIT) return withEvidence(createEmptyCheckpoint(), evidence);
|
|
73
|
+
parsed = JSON.parse(await handle.readFile("utf8"));
|
|
74
|
+
} finally {
|
|
75
|
+
await handle.close();
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
return withEvidence(createEmptyCheckpoint(), evidence);
|
|
79
|
+
}
|
|
80
|
+
if (!isRecord(parsed) || parsed.version !== 1) return withEvidence(createEmptyCheckpoint(), evidence);
|
|
81
|
+
return withEvidence(
|
|
82
|
+
{
|
|
83
|
+
version: 1,
|
|
84
|
+
completed: readStringList(parsed.completed),
|
|
85
|
+
remaining: readStringList(parsed.remaining),
|
|
86
|
+
summary: readString(parsed.summary, 2_000),
|
|
87
|
+
artifacts: readStringList(parsed.artifacts),
|
|
88
|
+
messageCount: 0,
|
|
89
|
+
outputTail: "",
|
|
90
|
+
lastEventAtMs: 0,
|
|
91
|
+
},
|
|
92
|
+
evidence,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildCheckpointTask(options: BuildCheckpointTaskOptions): string {
|
|
97
|
+
const prior = options.checkpoint;
|
|
98
|
+
const currentTask =
|
|
99
|
+
prior !== undefined && prior.remaining.length > 0
|
|
100
|
+
? [
|
|
101
|
+
"Only unresolved checkpoint units:",
|
|
102
|
+
...prior.remaining.map((unit, index) => `${index + 1}. ${boundedTail(unit, 512)}`),
|
|
103
|
+
].join("\n")
|
|
104
|
+
: boundedTail(options.shardTask, 3_500);
|
|
105
|
+
const priorSection = prior
|
|
106
|
+
? [
|
|
107
|
+
"Resume checkpoint:",
|
|
108
|
+
`- Completed: ${formatList(prior.completed)}`,
|
|
109
|
+
`- Remaining: ${formatList(prior.remaining)}`,
|
|
110
|
+
`- Summary: ${boundedTail(prior.summary, 1_500) || "(none)"}`,
|
|
111
|
+
`- Artifacts: ${formatList(prior.artifacts)}`,
|
|
112
|
+
`- Last streamed evidence: ${boundedTail(prior.outputTail, 2_500) || "(none)"}`,
|
|
113
|
+
]
|
|
114
|
+
: ["No prior checkpoint exists for this shard."];
|
|
115
|
+
return [
|
|
116
|
+
"[OMK deadline/checkpoint execution contract]",
|
|
117
|
+
`Execution shard: ${options.shardId} (${options.shardIndex + 1}/${options.shardCount}), attempt ${options.attempt}.`,
|
|
118
|
+
`This attempt has about ${options.cutoffMs}ms. Work in small, complete semantic units.`,
|
|
119
|
+
`After each completed unit, overwrite ${options.checkpointFilePath} with JSON:`,
|
|
120
|
+
'{"version":1,"completed":["..."],"remaining":["..."],"summary":"...","artifacts":["..."]}',
|
|
121
|
+
"Only checkpoint work that is actually complete. Keep each field concise.",
|
|
122
|
+
"On resume, inspect the current workspace first. Do not repeat completed side effects or rerun expensive discovery already evidenced below.",
|
|
123
|
+
...priorSection,
|
|
124
|
+
"",
|
|
125
|
+
"Original logical task (context only):",
|
|
126
|
+
boundedTail(options.originalTask, 2_500),
|
|
127
|
+
"",
|
|
128
|
+
"Current shard:",
|
|
129
|
+
currentTask,
|
|
130
|
+
"Return a self-contained result for this shard.",
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function checkpointMadeProgress(before: AgentCheckpoint | undefined, after: AgentCheckpoint): boolean {
|
|
135
|
+
if (before === undefined) {
|
|
136
|
+
return after.messageCount > 0 || after.completed.length > 0 || after.summary !== "" || after.outputTail !== "";
|
|
137
|
+
}
|
|
138
|
+
return (
|
|
139
|
+
after.messageCount > before.messageCount ||
|
|
140
|
+
after.outputTail !== before.outputTail ||
|
|
141
|
+
after.summary !== before.summary ||
|
|
142
|
+
!arrayEquals(after.completed, before.completed) ||
|
|
143
|
+
!arrayEquals(after.remaining, before.remaining) ||
|
|
144
|
+
!arrayEquals(after.artifacts, before.artifacts)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function boundedTail(value: string, maxChars: number): string {
|
|
149
|
+
if (value.length <= maxChars) return value;
|
|
150
|
+
return `[...${value.length - maxChars} earlier chars omitted...]\n${value.slice(-maxChars)}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function createEmptyCheckpoint(): AgentCheckpoint {
|
|
154
|
+
return {
|
|
155
|
+
version: 1,
|
|
156
|
+
completed: [],
|
|
157
|
+
remaining: [],
|
|
158
|
+
summary: "",
|
|
159
|
+
artifacts: [],
|
|
160
|
+
messageCount: 0,
|
|
161
|
+
outputTail: "",
|
|
162
|
+
lastEventAtMs: 0,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function withEvidence(checkpoint: AgentCheckpoint, evidence: CheckpointEvidence): AgentCheckpoint {
|
|
167
|
+
return {
|
|
168
|
+
...checkpoint,
|
|
169
|
+
messageCount: Math.max(0, evidence.messageCount),
|
|
170
|
+
outputTail: boundedTail(evidence.outputTail, 4_000),
|
|
171
|
+
lastEventAtMs: Math.max(0, evidence.lastEventAtMs),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readStringList(value: unknown): string[] {
|
|
176
|
+
if (!Array.isArray(value)) return [];
|
|
177
|
+
return value
|
|
178
|
+
.filter((item): item is string => typeof item === "string")
|
|
179
|
+
.map((item) => readString(item, MAX_ITEM_CHARS))
|
|
180
|
+
.filter((item) => item !== "")
|
|
181
|
+
.slice(0, MAX_LIST_ITEMS);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function readString(value: unknown, maxChars: number): string {
|
|
185
|
+
return typeof value === "string" ? value.trim().slice(0, maxChars) : "";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function formatList(values: readonly string[]): string {
|
|
189
|
+
return values.length === 0 ? "(none)" : values.map((value) => boundedTail(value, 256)).join("; ");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
193
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function arrayEquals(left: readonly string[], right: readonly string[]): boolean {
|
|
197
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
198
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
allocateAttemptBudget,
|
|
4
|
+
createExecutionBudget,
|
|
5
|
+
estimateTaskDemand,
|
|
6
|
+
getDeadlineProfile,
|
|
7
|
+
planTaskShards,
|
|
8
|
+
recommendAttemptCutoff,
|
|
9
|
+
remainingExecutionMs,
|
|
10
|
+
updateDeadlineProfiles,
|
|
11
|
+
} from "./deadline-budget.ts";
|
|
12
|
+
|
|
13
|
+
describe("subagent dual budget planning", () => {
|
|
14
|
+
it("shards an explicit action list when token and wall-clock demand exceed one attempt", () => {
|
|
15
|
+
const task = [
|
|
16
|
+
"Upgrade the runtime:",
|
|
17
|
+
"1. Inspect the provider boundary.",
|
|
18
|
+
"2. Add typed deadline metadata.",
|
|
19
|
+
"3. Implement checkpoint persistence.",
|
|
20
|
+
"4. Add bounded resume prompts.",
|
|
21
|
+
"5. Run fixture tests.",
|
|
22
|
+
"6. Review process cleanup.",
|
|
23
|
+
].join("\n");
|
|
24
|
+
const demand = estimateTaskDemand(task);
|
|
25
|
+
const shards = planTaskShards(task, demand, {
|
|
26
|
+
attemptCutoffMs: 180_000,
|
|
27
|
+
maxTaskShards: 3,
|
|
28
|
+
maxTokensPerShard: 40,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(demand.workUnits).toBeGreaterThanOrEqual(6);
|
|
32
|
+
expect(shards).toHaveLength(3);
|
|
33
|
+
expect(shards.flatMap((shard) => shard.units)).toEqual([
|
|
34
|
+
"Inspect the provider boundary.",
|
|
35
|
+
"Add typed deadline metadata.",
|
|
36
|
+
"Implement checkpoint persistence.",
|
|
37
|
+
"Add bounded resume prompts.",
|
|
38
|
+
"Run fixture tests.",
|
|
39
|
+
"Review process cleanup.",
|
|
40
|
+
]);
|
|
41
|
+
expect(shards.every((shard) => shard.task.includes("Do only this execution shard"))).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("recognizes explicit CJK numbered actions without relying on English verbs", () => {
|
|
45
|
+
const task = [
|
|
46
|
+
"런타임 업그레이드:",
|
|
47
|
+
"1. 프로세스 경계를 분석한다.",
|
|
48
|
+
"2. 체크포인트 재개를 구현한다.",
|
|
49
|
+
"3. fixture 테스트를 실행한다.",
|
|
50
|
+
].join("\n");
|
|
51
|
+
const demand = estimateTaskDemand(task);
|
|
52
|
+
const shards = planTaskShards(task, demand, {
|
|
53
|
+
attemptCutoffMs: 100_000,
|
|
54
|
+
maxTaskShards: 3,
|
|
55
|
+
maxTokensPerShard: 20,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(demand.actionUnits).toEqual([
|
|
59
|
+
"프로세스 경계를 분석한다.",
|
|
60
|
+
"체크포인트 재개를 구현한다.",
|
|
61
|
+
"fixture 테스트를 실행한다.",
|
|
62
|
+
]);
|
|
63
|
+
expect(shards.length).toBeGreaterThan(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("keeps an indivisible task as one shard even when it is expensive", () => {
|
|
67
|
+
const task = "Investigate and fix the runtime without repeating completed side effects.";
|
|
68
|
+
const demand = estimateTaskDemand(task);
|
|
69
|
+
const shards = planTaskShards(task, demand, {
|
|
70
|
+
attemptCutoffMs: 1,
|
|
71
|
+
maxTaskShards: 3,
|
|
72
|
+
maxTokensPerShard: 1,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(shards).toEqual([{ id: "shard-1", index: 0, task, units: [task] }]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("reserves cleanup time and divides a sequential deadline among pending work", () => {
|
|
79
|
+
const budget = createExecutionBudget({
|
|
80
|
+
nowMs: 1_000,
|
|
81
|
+
hardDeadlineMs: 900,
|
|
82
|
+
cleanupReserveMs: 100,
|
|
83
|
+
maxResumeAttempts: 1,
|
|
84
|
+
maxTaskShards: 3,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(remainingExecutionMs(budget, 1_250)).toBe(650);
|
|
88
|
+
expect(
|
|
89
|
+
allocateAttemptBudget({
|
|
90
|
+
budget,
|
|
91
|
+
nowMs: 1_250,
|
|
92
|
+
pendingSequentialTasks: 2,
|
|
93
|
+
pendingShards: 1,
|
|
94
|
+
recommendedCutoffMs: 1_000,
|
|
95
|
+
minimumAttemptMs: 1,
|
|
96
|
+
}),
|
|
97
|
+
).toEqual({ cutoffMs: 275, taskDeadlineAtMs: 1_525, usableMs: 550 });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("provider/model deadline learning", () => {
|
|
102
|
+
it("learns completion and cutoff history independently per provider and model", () => {
|
|
103
|
+
let db = updateDeadlineProfiles(undefined, {
|
|
104
|
+
provider: "openai-codex",
|
|
105
|
+
model: "gpt-5.6",
|
|
106
|
+
outcome: "completed",
|
|
107
|
+
elapsedMs: 120_000,
|
|
108
|
+
estimatedTokens: 2_000,
|
|
109
|
+
workUnits: 2,
|
|
110
|
+
});
|
|
111
|
+
db = updateDeadlineProfiles(db, {
|
|
112
|
+
provider: "openai-codex",
|
|
113
|
+
model: "gpt-5.6",
|
|
114
|
+
outcome: "cutoff",
|
|
115
|
+
elapsedMs: 600_000,
|
|
116
|
+
estimatedTokens: 2_000,
|
|
117
|
+
workUnits: 2,
|
|
118
|
+
});
|
|
119
|
+
db = updateDeadlineProfiles(db, {
|
|
120
|
+
provider: "anthropic",
|
|
121
|
+
model: "claude-sonnet",
|
|
122
|
+
outcome: "completed",
|
|
123
|
+
elapsedMs: 40_000,
|
|
124
|
+
estimatedTokens: 500,
|
|
125
|
+
workUnits: 1,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const codex = getDeadlineProfile(db, "openai-codex", "gpt-5.6");
|
|
129
|
+
const anthropic = getDeadlineProfile(db, "anthropic", "claude-sonnet");
|
|
130
|
+
expect(codex).toMatchObject({ samples: 2, completions: 1, cutoffs: 1 });
|
|
131
|
+
expect(anthropic).toMatchObject({ samples: 1, completions: 1, cutoffs: 0 });
|
|
132
|
+
expect(recommendAttemptCutoff(codex, { fallbackMs: 600_000, minimumMs: 1, maximumMs: 800_000 })).toBe(180_000);
|
|
133
|
+
expect(recommendAttemptCutoff(anthropic, { fallbackMs: 600_000, minimumMs: 1, maximumMs: 800_000 })).toBe(60_000);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
export type DeadlineSampleOutcome = "completed" | "cutoff" | "aborted" | "failed";
|
|
2
|
+
|
|
3
|
+
export interface TaskDemand {
|
|
4
|
+
readonly estimatedTokens: number;
|
|
5
|
+
readonly workUnits: number;
|
|
6
|
+
readonly predictedMs: number;
|
|
7
|
+
readonly actionUnits: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TaskShard {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly index: number;
|
|
13
|
+
readonly task: string;
|
|
14
|
+
readonly units: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ExecutionBudget {
|
|
18
|
+
readonly startedAtMs: number;
|
|
19
|
+
readonly hardDeadlineMs: number;
|
|
20
|
+
readonly deadlineAtMs: number;
|
|
21
|
+
readonly cleanupReserveMs: number;
|
|
22
|
+
readonly maxResumeAttempts: number;
|
|
23
|
+
readonly maxTaskShards: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DeadlineProfileSample {
|
|
27
|
+
readonly provider: string;
|
|
28
|
+
readonly model: string;
|
|
29
|
+
readonly outcome: DeadlineSampleOutcome;
|
|
30
|
+
readonly elapsedMs: number;
|
|
31
|
+
readonly estimatedTokens: number;
|
|
32
|
+
readonly workUnits: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DeadlineProfile {
|
|
36
|
+
readonly provider: string;
|
|
37
|
+
readonly model: string;
|
|
38
|
+
readonly samples: number;
|
|
39
|
+
readonly completions: number;
|
|
40
|
+
readonly cutoffs: number;
|
|
41
|
+
readonly aborts: number;
|
|
42
|
+
readonly failures: number;
|
|
43
|
+
readonly ewmaElapsedMs: number;
|
|
44
|
+
readonly ewmaMsPerWorkUnit: number;
|
|
45
|
+
readonly completedDurationsMs: readonly number[];
|
|
46
|
+
readonly cutoffDurationsMs: readonly number[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DeadlineProfileDb {
|
|
50
|
+
readonly version: 1;
|
|
51
|
+
readonly profiles: Readonly<Record<string, DeadlineProfile>>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface CreateExecutionBudgetOptions {
|
|
55
|
+
readonly nowMs?: number;
|
|
56
|
+
readonly hardDeadlineMs: number;
|
|
57
|
+
readonly cleanupReserveMs: number;
|
|
58
|
+
readonly maxResumeAttempts: number;
|
|
59
|
+
readonly maxTaskShards: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface PlanTaskShardsOptions {
|
|
63
|
+
readonly attemptCutoffMs: number;
|
|
64
|
+
readonly maxTaskShards: number;
|
|
65
|
+
readonly maxTokensPerShard: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface AllocateAttemptBudgetOptions {
|
|
69
|
+
readonly budget: ExecutionBudget;
|
|
70
|
+
readonly nowMs?: number;
|
|
71
|
+
readonly pendingSequentialTasks: number;
|
|
72
|
+
readonly pendingShards: number;
|
|
73
|
+
readonly recommendedCutoffMs: number;
|
|
74
|
+
readonly minimumAttemptMs: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface RecommendCutoffOptions {
|
|
78
|
+
readonly fallbackMs: number;
|
|
79
|
+
readonly minimumMs: number;
|
|
80
|
+
readonly maximumMs: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const EXPLICIT_ACTION_LINE = /^\s*(?:\d+[.)]|\[[ xX]\])\s+(.+?)\s*$/;
|
|
84
|
+
const BULLET_ACTION_LINE = /^\s*[-*+]\s+(.+?)\s*$/;
|
|
85
|
+
const ACTION_VERB =
|
|
86
|
+
/^(?:inspect|add|implement|create|fix|test|run|review|analy[sz]e|verify|update|refactor|migrate|remove|investigate|write|read|document|build|check)\b/i;
|
|
87
|
+
const MAX_PROFILE_SAMPLES = 24;
|
|
88
|
+
const PROFILE_ALPHA = 0.25;
|
|
89
|
+
|
|
90
|
+
export function estimateTokens(text: string): number {
|
|
91
|
+
return Math.max(1, Math.ceil(Buffer.byteLength(text, "utf8") / 4));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function estimateTaskDemand(task: string): TaskDemand {
|
|
95
|
+
const actionUnits = task
|
|
96
|
+
.split(/\r?\n/)
|
|
97
|
+
.map(extractActionUnit)
|
|
98
|
+
.filter((line): line is string => line !== undefined);
|
|
99
|
+
const estimatedTokens = estimateTokens(task);
|
|
100
|
+
const pathSignals = new Set(task.match(/(?:\.?\.?\/)?[\w.-]+(?:\/[\w.-]+)+/g) ?? []).size;
|
|
101
|
+
const verificationSignals = (task.match(/\b(?:test|typecheck|build|lint|review|verify)\b/gi) ?? []).length;
|
|
102
|
+
const workUnits = Math.max(1, actionUnits.length, Math.min(12, pathSignals + verificationSignals));
|
|
103
|
+
const predictedMs = 45_000 + workUnits * 75_000 + Math.min(120_000, estimatedTokens * 20);
|
|
104
|
+
return { estimatedTokens, workUnits, predictedMs, actionUnits };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function planTaskShards(task: string, demand: TaskDemand, options: PlanTaskShardsOptions): TaskShard[] {
|
|
108
|
+
if (demand.actionUnits.length < 2) return [{ id: "shard-1", index: 0, task, units: [task] }];
|
|
109
|
+
const tokenShards = Math.ceil(demand.estimatedTokens / Math.max(1, options.maxTokensPerShard));
|
|
110
|
+
const timeShards = Math.ceil(demand.predictedMs / Math.max(1, options.attemptCutoffMs));
|
|
111
|
+
const shardCount = Math.min(options.maxTaskShards, demand.actionUnits.length, Math.max(1, tokenShards, timeShards));
|
|
112
|
+
if (shardCount === 1) return [{ id: "shard-1", index: 0, task, units: demand.actionUnits }];
|
|
113
|
+
|
|
114
|
+
const shards: TaskShard[] = [];
|
|
115
|
+
for (let index = 0; index < shardCount; index++) {
|
|
116
|
+
const start = Math.floor((index * demand.actionUnits.length) / shardCount);
|
|
117
|
+
const end = Math.floor(((index + 1) * demand.actionUnits.length) / shardCount);
|
|
118
|
+
const units = demand.actionUnits.slice(start, end);
|
|
119
|
+
const current = units.map((unit, unitIndex) => `${unitIndex + 1}. ${unit}`).join("\n");
|
|
120
|
+
shards.push({
|
|
121
|
+
id: `shard-${index + 1}`,
|
|
122
|
+
index,
|
|
123
|
+
units,
|
|
124
|
+
task: [
|
|
125
|
+
"Original task context:",
|
|
126
|
+
task,
|
|
127
|
+
"",
|
|
128
|
+
`Do only this execution shard (${index + 1}/${shardCount}):`,
|
|
129
|
+
current,
|
|
130
|
+
"Do not execute later shards; leave them for the runtime scheduler.",
|
|
131
|
+
].join("\n"),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return shards;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createExecutionBudget(options: CreateExecutionBudgetOptions): ExecutionBudget {
|
|
138
|
+
const startedAtMs = options.nowMs ?? Date.now();
|
|
139
|
+
const hardDeadlineMs = Math.max(1, Math.floor(options.hardDeadlineMs));
|
|
140
|
+
return {
|
|
141
|
+
startedAtMs,
|
|
142
|
+
hardDeadlineMs,
|
|
143
|
+
deadlineAtMs: startedAtMs + hardDeadlineMs,
|
|
144
|
+
cleanupReserveMs: Math.max(0, Math.floor(options.cleanupReserveMs)),
|
|
145
|
+
maxResumeAttempts: Math.max(0, Math.floor(options.maxResumeAttempts)),
|
|
146
|
+
maxTaskShards: Math.max(1, Math.floor(options.maxTaskShards)),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function remainingExecutionMs(budget: ExecutionBudget, nowMs = Date.now()): number {
|
|
151
|
+
return Math.max(0, budget.deadlineAtMs - nowMs);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function allocateAttemptBudget(options: AllocateAttemptBudgetOptions): {
|
|
155
|
+
readonly cutoffMs: number;
|
|
156
|
+
readonly taskDeadlineAtMs: number;
|
|
157
|
+
readonly usableMs: number;
|
|
158
|
+
} {
|
|
159
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
160
|
+
const usableMs = Math.max(0, remainingExecutionMs(options.budget, nowMs) - options.budget.cleanupReserveMs);
|
|
161
|
+
const taskShareMs = Math.floor(usableMs / Math.max(1, options.pendingSequentialTasks));
|
|
162
|
+
const shardShareMs = Math.floor(taskShareMs / Math.max(1, options.pendingShards));
|
|
163
|
+
const cutoffMs = Math.max(0, Math.min(options.recommendedCutoffMs, shardShareMs));
|
|
164
|
+
return {
|
|
165
|
+
cutoffMs: cutoffMs >= options.minimumAttemptMs ? cutoffMs : 0,
|
|
166
|
+
taskDeadlineAtMs: nowMs + taskShareMs,
|
|
167
|
+
usableMs,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function profileKey(provider: string, model: string): string {
|
|
172
|
+
return `${provider.trim() || "unknown"}/${model.trim() || "default"}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function getDeadlineProfile(
|
|
176
|
+
db: DeadlineProfileDb | undefined,
|
|
177
|
+
provider: string,
|
|
178
|
+
model: string,
|
|
179
|
+
): DeadlineProfile | undefined {
|
|
180
|
+
return db?.profiles[profileKey(provider, model)];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function updateDeadlineProfiles(
|
|
184
|
+
db: DeadlineProfileDb | undefined,
|
|
185
|
+
sample: DeadlineProfileSample,
|
|
186
|
+
): DeadlineProfileDb {
|
|
187
|
+
const key = profileKey(sample.provider, sample.model);
|
|
188
|
+
const previous = db?.profiles[key];
|
|
189
|
+
const elapsedMs = Math.max(1, Math.floor(sample.elapsedMs));
|
|
190
|
+
const workUnits = Math.max(1, sample.workUnits);
|
|
191
|
+
const next: DeadlineProfile = {
|
|
192
|
+
provider: sample.provider,
|
|
193
|
+
model: sample.model,
|
|
194
|
+
samples: (previous?.samples ?? 0) + 1,
|
|
195
|
+
completions: (previous?.completions ?? 0) + Number(sample.outcome === "completed"),
|
|
196
|
+
cutoffs: (previous?.cutoffs ?? 0) + Number(sample.outcome === "cutoff"),
|
|
197
|
+
aborts: (previous?.aborts ?? 0) + Number(sample.outcome === "aborted"),
|
|
198
|
+
failures: (previous?.failures ?? 0) + Number(sample.outcome === "failed"),
|
|
199
|
+
ewmaElapsedMs: ewma(previous?.ewmaElapsedMs, elapsedMs),
|
|
200
|
+
ewmaMsPerWorkUnit: ewma(previous?.ewmaMsPerWorkUnit, elapsedMs / workUnits),
|
|
201
|
+
completedDurationsMs: appendBounded(
|
|
202
|
+
previous?.completedDurationsMs,
|
|
203
|
+
sample.outcome === "completed" ? elapsedMs : undefined,
|
|
204
|
+
),
|
|
205
|
+
cutoffDurationsMs: appendBounded(
|
|
206
|
+
previous?.cutoffDurationsMs,
|
|
207
|
+
sample.outcome === "cutoff" ? elapsedMs : undefined,
|
|
208
|
+
),
|
|
209
|
+
};
|
|
210
|
+
return { version: 1, profiles: { ...(db?.profiles ?? {}), [key]: next } };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function recommendAttemptCutoff(profile: DeadlineProfile | undefined, options: RecommendCutoffOptions): number {
|
|
214
|
+
const completed = percentile(profile?.completedDurationsMs ?? [], 0.9);
|
|
215
|
+
const cutoff = percentile(profile?.cutoffDurationsMs ?? [], 0.25);
|
|
216
|
+
const candidates = [options.fallbackMs];
|
|
217
|
+
if (completed !== undefined) candidates.push(Math.round(completed * 1.5));
|
|
218
|
+
if (cutoff !== undefined) candidates.push(Math.round(cutoff * 0.75));
|
|
219
|
+
return Math.max(options.minimumMs, Math.min(options.maximumMs, ...candidates));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function extractActionUnit(line: string): string | undefined {
|
|
223
|
+
const explicit = line.match(EXPLICIT_ACTION_LINE)?.[1]?.trim();
|
|
224
|
+
if (explicit) return explicit;
|
|
225
|
+
const bullet = line.match(BULLET_ACTION_LINE)?.[1]?.trim();
|
|
226
|
+
return bullet && ACTION_VERB.test(bullet) ? bullet : undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function ewma(previous: number | undefined, value: number): number {
|
|
230
|
+
return Math.round(previous === undefined ? value : previous * (1 - PROFILE_ALPHA) + value * PROFILE_ALPHA);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function appendBounded(previous: readonly number[] | undefined, value: number | undefined): number[] {
|
|
234
|
+
if (value === undefined) return [...(previous ?? [])];
|
|
235
|
+
return [...(previous ?? []), value].slice(-MAX_PROFILE_SAMPLES);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function percentile(values: readonly number[], ratio: number): number | undefined {
|
|
239
|
+
if (values.length === 0) return undefined;
|
|
240
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
241
|
+
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * ratio))];
|
|
242
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { DeadlineProfileStore } from "./deadline-profile-store.ts";
|
|
6
|
+
|
|
7
|
+
const cleanupPaths: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
await Promise.all(cleanupPaths.splice(0).map((entry) => fs.promises.rm(entry, { force: true, recursive: true })));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("DeadlineProfileStore", () => {
|
|
14
|
+
it("persists provider/model cutoff history across dispatcher processes", async () => {
|
|
15
|
+
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "omk-deadline-profile-"));
|
|
16
|
+
cleanupPaths.push(dir);
|
|
17
|
+
const filePath = path.join(dir, "profiles.json");
|
|
18
|
+
const first = new DeadlineProfileStore(filePath);
|
|
19
|
+
await first.record({
|
|
20
|
+
provider: "openai-codex",
|
|
21
|
+
model: "gpt-5.6",
|
|
22
|
+
outcome: "cutoff",
|
|
23
|
+
elapsedMs: 500,
|
|
24
|
+
estimatedTokens: 100,
|
|
25
|
+
workUnits: 2,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const reloaded = new DeadlineProfileStore(filePath);
|
|
29
|
+
expect(await reloaded.profileFor("openai-codex", "gpt-5.6")).toMatchObject({ samples: 1, cutoffs: 1 });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("serializes concurrent samples without losing updates", async () => {
|
|
33
|
+
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "omk-deadline-concurrent-"));
|
|
34
|
+
cleanupPaths.push(dir);
|
|
35
|
+
const filePath = path.join(dir, "profiles.json");
|
|
36
|
+
const stores = [new DeadlineProfileStore(filePath), new DeadlineProfileStore(filePath)];
|
|
37
|
+
await Promise.all(
|
|
38
|
+
Array.from({ length: 8 }, (_, index) =>
|
|
39
|
+
stores[index % stores.length].record({
|
|
40
|
+
provider: "anthropic",
|
|
41
|
+
model: "claude-sonnet",
|
|
42
|
+
outcome: "completed",
|
|
43
|
+
elapsedMs: 100 + index,
|
|
44
|
+
estimatedTokens: 10,
|
|
45
|
+
workUnits: 1,
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const reloaded = new DeadlineProfileStore(filePath);
|
|
51
|
+
expect(await reloaded.profileFor("anthropic", "claude-sonnet")).toMatchObject({ samples: 8, completions: 8 });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("recovers from malformed history without exposing untyped data", async () => {
|
|
55
|
+
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "omk-deadline-corrupt-"));
|
|
56
|
+
cleanupPaths.push(dir);
|
|
57
|
+
const filePath = path.join(dir, "profiles.json");
|
|
58
|
+
await fs.promises.writeFile(filePath, "{not-json", "utf8");
|
|
59
|
+
const store = new DeadlineProfileStore(filePath);
|
|
60
|
+
|
|
61
|
+
expect(await store.profileFor("unknown", "default")).toBeUndefined();
|
|
62
|
+
await store.record({
|
|
63
|
+
provider: "unknown",
|
|
64
|
+
model: "default",
|
|
65
|
+
outcome: "failed",
|
|
66
|
+
elapsedMs: 20,
|
|
67
|
+
estimatedTokens: 5,
|
|
68
|
+
workUnits: 1,
|
|
69
|
+
});
|
|
70
|
+
expect(JSON.parse(await fs.promises.readFile(filePath, "utf8"))).toMatchObject({ version: 1 });
|
|
71
|
+
});
|
|
72
|
+
});
|