@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.3
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/LICENSE +21 -0
- package/README.md +141 -2
- package/bin/codeflow.mjs +8 -0
- package/bin/launcher.mjs +127 -0
- package/dist/remote-host.json +1 -0
- package/licenses/inter.txt +92 -0
- package/licenses/preact.txt +21 -0
- package/licenses/signals-core.txt +21 -0
- package/licenses/signals.txt +21 -0
- package/licenses/typescript-notices.txt +193 -0
- package/licenses/typescript.txt +55 -0
- package/licenses/uqr.txt +22 -0
- package/licenses/xterm-fit.txt +19 -0
- package/licenses/xterm.txt +21 -0
- package/package.json +34 -4
- package/public/assets/changes-62ZIEQSN.js +1 -0
- package/public/assets/chunk-2PZVOKID.js +1 -0
- package/public/assets/chunk-4D5IENEB.js +1 -0
- package/public/assets/chunk-5SARP5MU.js +1 -0
- package/public/assets/chunk-63YLZ3PR.js +1 -0
- package/public/assets/chunk-7FUHKF2M.js +1 -0
- package/public/assets/chunk-APQEVJ2K.js +1 -0
- package/public/assets/chunk-CGOZQMEZ.js +2 -0
- package/public/assets/chunk-J4VYS3KV.js +2 -0
- package/public/assets/chunk-L7TUCIYF.js +1 -0
- package/public/assets/chunk-SIHDWM2Y.js +154 -0
- package/public/assets/chunk-TEJVYIKC.js +1 -0
- package/public/assets/code-browser-VB2HGZSG.js +4 -0
- package/public/assets/filtered-tasks-ES5CP4KV.js +1 -0
- package/public/assets/interactive-browser-7F5ZXRUV.js +7 -0
- package/public/assets/join-project-6RTFHNCA.js +1 -0
- package/public/assets/main-65EW73SB.js +1 -0
- package/public/assets/project-invitations-view-GMPWGWYM.js +1 -0
- package/public/assets/settings-UB5Q4AQM.js +9 -0
- package/public/assets/styles-18792BB4.css +32 -0
- package/public/assets/team-server-wizard-2GLDWS6D.js +8 -0
- package/public/assets/terminal-IWAIM4DE.js +62 -0
- package/public/assets.json +1 -0
- package/public/bundle-meta.json +1 -0
- package/public/docs/.nojekyll +0 -0
- package/public/docs/accessibility.html +19 -0
- package/public/docs/acp-agents.html +117 -0
- package/public/docs/agent-containers.html +79 -0
- package/public/docs/api.html +1130 -0
- package/public/docs/app.js +342 -0
- package/public/docs/application-improvements.html +275 -0
- package/public/docs/architecture-explorer.html +19 -0
- package/public/docs/architecture.html +149 -0
- package/public/docs/auto-team.html +148 -0
- package/public/docs/automations.html +109 -0
- package/public/docs/checkpoint-review.html +37 -0
- package/public/docs/code-hosts.html +35 -0
- package/public/docs/code-intelligence.html +35 -0
- package/public/docs/configuration.html +346 -0
- package/public/docs/context-inspection.html +23 -0
- package/public/docs/coordinated-changes.html +28 -0
- package/public/docs/deferred-runs.html +25 -0
- package/public/docs/dependency-discovery.html +19 -0
- package/public/docs/documentation.html +84 -0
- package/public/docs/dynamic-teams-evaluation.html +174 -0
- package/public/docs/dynamic-teams-implementation-plan.html +882 -0
- package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
- package/public/docs/dynamic-teams-plan-review-0.html +204 -0
- package/public/docs/extensions.html +42 -0
- package/public/docs/external-sources.html +96 -0
- package/public/docs/filesystem-policy.html +26 -0
- package/public/docs/filtered-tasks.html +44 -0
- package/public/docs/font-license.txt +92 -0
- package/public/docs/getting-started.html +143 -0
- package/public/docs/github-review-threads.html +26 -0
- package/public/docs/github.html +157 -0
- package/public/docs/help.html +69 -0
- package/public/docs/index.html +88 -0
- package/public/docs/installation.html +80 -0
- package/public/docs/launch-profiles.html +48 -0
- package/public/docs/maintenance.html +60 -0
- package/public/docs/memory-claims.html +31 -0
- package/public/docs/memory-storage.html +58 -0
- package/public/docs/notifications.html +39 -0
- package/public/docs/patterns.html +195 -0
- package/public/docs/performance-results.html +210 -0
- package/public/docs/performance.html +166 -0
- package/public/docs/project-conventions.html +22 -0
- package/public/docs/project-first-navigation-plan.html +587 -0
- package/public/docs/project-teams-implementation-plan.html +772 -0
- package/public/docs/project-teams.html +129 -0
- package/public/docs/provider-compatibility.html +20 -0
- package/public/docs/releasing.html +65 -0
- package/public/docs/remote-workers.html +64 -0
- package/public/docs/review-inbox.html +26 -0
- package/public/docs/run-storage.html +23 -0
- package/public/docs/runs.html +141 -0
- package/public/docs/search.json +1 -0
- package/public/docs/security.html +133 -0
- package/public/docs/shared-projects.html +31 -0
- package/public/docs/ssh-projects.html +60 -0
- package/public/docs/style.css +768 -0
- package/public/docs/task-reruns.html +30 -0
- package/public/docs/team-collaboration.html +412 -0
- package/public/docs/verification.html +22 -0
- package/public/docs/vscode.html +53 -0
- package/public/docs/worker-permission-verification.html +131 -0
- package/public/index.html +17 -0
- package/scripts/check-node.mjs +7 -0
- package/scripts/codeflow.mjs +55 -0
- package/scripts/computer-use.swift +64 -0
- package/scripts/pty.py +273 -0
- package/src/acp-connections.js +110 -0
- package/src/adapters/acp-presets.js +37 -0
- package/src/adapters/acp-types.js +0 -0
- package/src/adapters/acp.js +328 -0
- package/src/adapters/agent-tools.js +28 -0
- package/src/adapters/agy-auth.js +67 -0
- package/src/adapters/agy-isolation.js +242 -0
- package/src/adapters/agy-tools.js +45 -0
- package/src/adapters/agy.js +326 -0
- package/src/adapters/claude-stream.js +58 -0
- package/src/adapters/claude.js +282 -0
- package/src/adapters/codex-app-server.js +555 -0
- package/src/adapters/codex.js +311 -0
- package/src/adapters/errors.js +57 -0
- package/src/adapters/index.js +91 -0
- package/src/adapters/managed-permissions.js +37 -0
- package/src/adapters/model-discovery.js +100 -0
- package/src/adapters/process.js +433 -0
- package/src/adapters/team-tools.js +103 -0
- package/src/adapters/types.js +0 -0
- package/src/agent-browser.js +688 -0
- package/src/agent-computer.js +88 -0
- package/src/agent-container.js +155 -0
- package/src/agent-team-definitions.js +131 -0
- package/src/agent-tool-client.mjs +79 -0
- package/src/agent-tools.js +296 -0
- package/src/approvals.js +134 -0
- package/src/architecture.js +456 -0
- package/src/assets.js +44 -0
- package/src/automation.js +901 -0
- package/src/browser-files.js +293 -0
- package/src/browser-ownership.js +150 -0
- package/src/certificate-renewal.js +75 -0
- package/src/chat-family.js +25 -0
- package/src/chat-handoff.js +87 -0
- package/src/checkpoint-review.js +72 -0
- package/src/checks.js +303 -0
- package/src/code-host-git.js +102 -0
- package/src/code-hosts-api.js +99 -0
- package/src/code-hosts.js +482 -0
- package/src/code-intelligence-worker.mjs +452 -0
- package/src/code-intelligence.js +141 -0
- package/src/collab/api.js +365 -0
- package/src/collab/checkpoints.js +198 -0
- package/src/collab/connections.js +139 -0
- package/src/collab/host-execution.js +347 -0
- package/src/collab/hub-client.js +210 -0
- package/src/collab/link-api.js +229 -0
- package/src/collab/link.js +595 -0
- package/src/collab/model.js +496 -0
- package/src/collab/runner.js +1014 -0
- package/src/collab/store.js +1452 -0
- package/src/collab/terminals.js +361 -0
- package/src/collaboration-reviews.js +116 -0
- package/src/config.js +48 -0
- package/src/context-budget.js +14 -0
- package/src/context-inspection.js +204 -0
- package/src/context.js +154 -0
- package/src/conventions.js +283 -0
- package/src/coordinated-api.js +247 -0
- package/src/deferred-launch.js +82 -0
- package/src/deferred-runs.js +515 -0
- package/src/dependency-health.js +35 -0
- package/src/diff.js +43 -0
- package/src/docs-help.js +316 -0
- package/src/doctor.js +232 -0
- package/src/documentation-changes.js +20 -0
- package/src/engine.js +5258 -0
- package/src/env.js +44 -0
- package/src/execution-budget.js +8 -0
- package/src/experience.js +50 -0
- package/src/extensions.js +144 -0
- package/src/filesystem-access.js +107 -0
- package/src/filesystem-policy.js +238 -0
- package/src/filtered-task-limits.js +93 -0
- package/src/filtered-tasks-api.js +53 -0
- package/src/filtered-tasks.js +576 -0
- package/src/frame-policy.js +66 -0
- package/src/fs.js +57 -0
- package/src/git.js +626 -0
- package/src/github-review-threads.js +181 -0
- package/src/github.js +538 -0
- package/src/http.js +69 -0
- package/src/improve-settings.js +38 -0
- package/src/improve.js +187 -0
- package/src/integrations.js +373 -0
- package/src/integrity.js +259 -0
- package/src/journey-api.js +77 -0
- package/src/journey-replay.js +65 -0
- package/src/journey-store.js +97 -0
- package/src/journeys.js +60 -0
- package/src/json.js +81 -0
- package/src/launch-profiles.js +292 -0
- package/src/learnings.js +80 -0
- package/src/local-app-url.js +19 -0
- package/src/log.js +39 -0
- package/src/maintenance.js +335 -0
- package/src/memory-analysis.js +182 -0
- package/src/memory-claims.js +153 -0
- package/src/memory-sqlite.js +236 -0
- package/src/memory.js +552 -0
- package/src/merge-validation.js +58 -0
- package/src/model-catalog.js +239 -0
- package/src/model-research.js +91 -0
- package/src/notes.js +176 -0
- package/src/notifications.js +413 -0
- package/src/patterns/auto-team-model.js +230 -0
- package/src/patterns/auto-team.js +816 -0
- package/src/patterns/council.js +199 -0
- package/src/patterns/debate.js +188 -0
- package/src/patterns/findings.js +141 -0
- package/src/patterns/index.js +21 -0
- package/src/patterns/loop.js +158 -0
- package/src/patterns/pipeline.js +125 -0
- package/src/patterns/prompts.js +181 -0
- package/src/patterns/race.js +147 -0
- package/src/patterns/schemas.js +40 -0
- package/src/patterns/solo.js +46 -0
- package/src/patterns/supervisor.js +235 -0
- package/src/patterns/task-draft.js +29 -0
- package/src/patterns/task-review.js +45 -0
- package/src/patterns/tdd.js +231 -0
- package/src/patterns/team-strategies.js +67 -0
- package/src/performance.js +43 -0
- package/src/persistence-status.js +35 -0
- package/src/phone-sessions.js +96 -0
- package/src/preview-overlay.js +392 -0
- package/src/preview.js +230 -0
- package/src/privacy.js +23 -0
- package/src/project-report.js +22 -0
- package/src/project-team-budget.js +22 -0
- package/src/project-team-model.js +225 -0
- package/src/project-team-policy.js +42 -0
- package/src/project-team-store.js +234 -0
- package/src/project-team-usage.js +24 -0
- package/src/project-teams-api.js +43 -0
- package/src/project-teams.js +644 -0
- package/src/projects.js +290 -0
- package/src/provider-eligibility.js +9 -0
- package/src/readiness.js +98 -0
- package/src/remote-tls.js +66 -0
- package/src/remote-workers/bundles.js +198 -0
- package/src/remote-workers/client.js +332 -0
- package/src/remote-workers/durability.js +37 -0
- package/src/remote-workers/protocol.js +68 -0
- package/src/remote-workers/stdio.js +138 -0
- package/src/remote-workers/worker.js +304 -0
- package/src/remote.js +204 -0
- package/src/repo-config.js +201 -0
- package/src/repo-coordination.js +725 -0
- package/src/repo-tree.js +106 -0
- package/src/repository-identity.js +13 -0
- package/src/retention-policy.js +466 -0
- package/src/review-inbox.js +38 -0
- package/src/routing.js +49 -0
- package/src/run-analysis.js +111 -0
- package/src/run-cold-store.js +231 -0
- package/src/run-environment.js +15 -0
- package/src/run-history.js +91 -0
- package/src/run-log.js +160 -0
- package/src/run-permissions.js +111 -0
- package/src/run-projection.js +77 -0
- package/src/run-recovery.js +53 -0
- package/src/run-status.js +56 -0
- package/src/serve.js +138 -0
- package/src/server.js +2871 -0
- package/src/shared-access.js +103 -0
- package/src/shared-accounts.js +381 -0
- package/src/shared-api.js +239 -0
- package/src/shell.js +346 -0
- package/src/ship.js +344 -0
- package/src/slots.js +74 -0
- package/src/snapshot.js +388 -0
- package/src/source-context.js +17 -0
- package/src/sse-writer.js +70 -0
- package/src/ssh-projects/bootstrap.mjs +95 -0
- package/src/ssh-projects/config.js +45 -0
- package/src/ssh-projects/gateway.js +143 -0
- package/src/ssh-projects/manager.js +320 -0
- package/src/ssh-projects/team-autostart.mjs +93 -0
- package/src/ssh-projects/team-remove.mjs +69 -0
- package/src/ssh-projects/team-status.mjs +39 -0
- package/src/ssh-projects/transport.js +133 -0
- package/src/stats.js +139 -0
- package/src/store.js +544 -0
- package/src/stream.js +91 -0
- package/src/sweeps.js +241 -0
- package/src/tailscale.js +101 -0
- package/src/task-draft.js +23 -0
- package/src/task-team.js +164 -0
- package/src/team-evaluation.js +243 -0
- package/src/team-mailbox.js +160 -0
- package/src/team-memory.js +84 -0
- package/src/team-rerun-graph.js +15 -0
- package/src/team-reruns.js +130 -0
- package/src/team-scheduler.js +121 -0
- package/src/team-steering.js +21 -0
- package/src/team-template-store.js +98 -0
- package/src/team-templates.js +117 -0
- package/src/team-tool-client.mjs +79 -0
- package/src/team-tools.js +235 -0
- package/src/teams.js +0 -0
- package/src/templates.js +62 -0
- package/src/transcript.js +134 -0
- package/src/verification-integrity.js +27 -0
- package/src/verification.js +183 -0
- package/src/watch.js +195 -0
- package/src/work-queue.js +47 -0
- package/src/workflow-control.js +83 -0
- package/src/workspace-api.js +207 -0
- package/src/workspace-changes.js +61 -0
- package/src/workspace.js +146 -0
package/src/routing.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { getAdapter, AGENT_IDS } from "./adapters/index.js";
|
|
2
|
+
const PREFERENCE = ["claude", "codex", "agy"];
|
|
3
|
+
const NEARLY_FULL = 0.9;
|
|
4
|
+
const LIMITED_FOR_MS = 60 * 6e4;
|
|
5
|
+
const SIGNED_OUT_FOR_MS = 15 * 6e4;
|
|
6
|
+
function exhausted(agent, quota, now = Date.now()) {
|
|
7
|
+
const q = quota[agent];
|
|
8
|
+
if (!q) return void 0;
|
|
9
|
+
if (q.status === "signed_out") return now - q.at < SIGNED_OUT_FOR_MS ? "isn't signed in" : void 0;
|
|
10
|
+
const windows = Object.entries(q.windows);
|
|
11
|
+
const live = windows.filter(([, w]) => !w.resetsAt || w.resetsAt > now);
|
|
12
|
+
if (q.status === "limited" || q.status === "rejected") {
|
|
13
|
+
const resets = windows.map(([, w]) => w.resetsAt).filter((t) => !!t);
|
|
14
|
+
if (resets.length ? Math.max(...resets) > now : now - q.at < LIMITED_FOR_MS) return "hit its usage limit";
|
|
15
|
+
}
|
|
16
|
+
for (const [name, w] of live) {
|
|
17
|
+
if ((w.utilization ?? 0) >= NEARLY_FULL) return `is at ${Math.round((w.utilization ?? 0) * 100)}% of its ${name.replace(/_/g, "-")} limit`;
|
|
18
|
+
}
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
function resolveAuto(roles, quota, installed, rank = { order: [], why: {} }) {
|
|
22
|
+
const notes = [];
|
|
23
|
+
const out = {};
|
|
24
|
+
const order = [.../* @__PURE__ */ new Set([...rank.order, ...PREFERENCE, ...AGENT_IDS])];
|
|
25
|
+
const candidates = order.filter((a) => installed.has(a));
|
|
26
|
+
const roomy = candidates.filter((a) => !exhausted(a, quota));
|
|
27
|
+
const skipped = candidates.filter((a) => exhausted(a, quota));
|
|
28
|
+
for (const [role, list] of Object.entries(roles)) {
|
|
29
|
+
const chosen = list.filter((a) => AGENT_IDS.includes(a));
|
|
30
|
+
for (const value of list) {
|
|
31
|
+
if (value !== "auto") continue;
|
|
32
|
+
const pick = roomy.find((a) => !chosen.includes(a)) ?? candidates.find((a) => !chosen.includes(a));
|
|
33
|
+
if (!pick) continue;
|
|
34
|
+
chosen.push(pick);
|
|
35
|
+
const reasons = [];
|
|
36
|
+
const pickedFull = exhausted(pick, quota);
|
|
37
|
+
if (pickedFull) reasons.push(`every agent is out of room; ${getAdapter(pick).displayName} ${pickedFull}`);
|
|
38
|
+
else if (skipped.length) reasons.push(`${getAdapter(skipped[0]).displayName} ${exhausted(skipped[0], quota)}`);
|
|
39
|
+
if (rank.why[pick] && rank.order[0] === pick) reasons.push(rank.why[pick]);
|
|
40
|
+
notes.push(`Auto picked ${getAdapter(pick).displayName} for ${role}${reasons.length ? ` (${reasons.join("; ")})` : ""}`);
|
|
41
|
+
}
|
|
42
|
+
out[role] = [...new Set(chosen)];
|
|
43
|
+
}
|
|
44
|
+
return { roles: out, notes };
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
exhausted,
|
|
48
|
+
resolveAuto
|
|
49
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
2
|
+
function interval(start, end) {
|
|
3
|
+
return finite(start) && finite(end) && end >= start ? [start, end] : void 0;
|
|
4
|
+
}
|
|
5
|
+
function stepInterval(step, now) {
|
|
6
|
+
return interval(step.startedAt, step.status === "running" ? now : step.endedAt);
|
|
7
|
+
}
|
|
8
|
+
function unionMs(intervals) {
|
|
9
|
+
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
|
|
10
|
+
let total = 0;
|
|
11
|
+
let start;
|
|
12
|
+
let end = 0;
|
|
13
|
+
for (const [a, b] of sorted) {
|
|
14
|
+
if (start === void 0) {
|
|
15
|
+
start = a;
|
|
16
|
+
end = b;
|
|
17
|
+
} else if (a > end) {
|
|
18
|
+
total += end - start;
|
|
19
|
+
start = a;
|
|
20
|
+
end = b;
|
|
21
|
+
} else end = Math.max(end, b);
|
|
22
|
+
}
|
|
23
|
+
return total + (start === void 0 ? 0 : end - start);
|
|
24
|
+
}
|
|
25
|
+
function analyzeWorkflow(team, input = {}) {
|
|
26
|
+
const now = input.now ?? Date.now();
|
|
27
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
28
|
+
const warnings = [];
|
|
29
|
+
for (const task of team.tasks) {
|
|
30
|
+
if (tasks.has(task.id)) warnings.push(`Duplicate task ID: ${task.id}`);
|
|
31
|
+
else tasks.set(task.id, task);
|
|
32
|
+
}
|
|
33
|
+
const edges = [];
|
|
34
|
+
const indegree = /* @__PURE__ */ new Map();
|
|
35
|
+
const children = /* @__PURE__ */ new Map();
|
|
36
|
+
for (const task of tasks.values()) {
|
|
37
|
+
const dependencies = [...new Set(task.dependsOn)];
|
|
38
|
+
indegree.set(task.id, dependencies.filter((id) => tasks.has(id)).length);
|
|
39
|
+
for (const dependency of dependencies) {
|
|
40
|
+
if (!tasks.has(dependency)) {
|
|
41
|
+
warnings.push(`${task.id} depends on missing task ${dependency}`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
edges.push({ from: dependency, to: task.id, kind: task.dependencyTypes?.[dependency] ?? "completion" });
|
|
45
|
+
children.set(dependency, [...children.get(dependency) ?? [], task.id]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const queue = [...tasks.keys()].filter((id) => indegree.get(id) === 0).sort();
|
|
49
|
+
const order = [];
|
|
50
|
+
const layer = /* @__PURE__ */ new Map();
|
|
51
|
+
while (queue.length) {
|
|
52
|
+
const id = queue.shift();
|
|
53
|
+
order.push(id);
|
|
54
|
+
layer.set(id, Math.max(0, ...tasks.get(id).dependsOn.filter((dep) => layer.has(dep)).map((dep) => layer.get(dep) + 1)));
|
|
55
|
+
for (const child of children.get(id) ?? []) {
|
|
56
|
+
indegree.set(child, indegree.get(child) - 1);
|
|
57
|
+
if (indegree.get(child) === 0) {
|
|
58
|
+
queue.push(child);
|
|
59
|
+
queue.sort();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (order.length < tasks.size) warnings.push("Dependencies contain a cycle. Edit the plan before dispatch.");
|
|
64
|
+
const durations = /* @__PURE__ */ new Map();
|
|
65
|
+
for (const task of tasks.values()) {
|
|
66
|
+
const steps = input.steps?.filter((step) => step.taskId === task.id);
|
|
67
|
+
if (steps?.length) {
|
|
68
|
+
const intervals = steps.map((step) => stepInterval(step, now));
|
|
69
|
+
if (intervals.every((value) => value !== void 0)) durations.set(task.id, intervals.reduce((sum, value) => sum + value[1] - value[0], 0));
|
|
70
|
+
} else {
|
|
71
|
+
const observed = interval(task.startedAt, task.status === "running" ? now : task.finishedAt);
|
|
72
|
+
if (observed) durations.set(task.id, observed[1] - observed[0]);
|
|
73
|
+
if ((task.attempts ?? 0) > 1) warnings.push(`${task.id}: task timestamps describe the latest attempt; earlier attempts are not included.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const scores = /* @__PURE__ */ new Map();
|
|
77
|
+
const chains = /* @__PURE__ */ new Map();
|
|
78
|
+
for (const id of order) {
|
|
79
|
+
const predecessors = tasks.get(id).dependsOn.filter((dep) => scores.has(dep)).sort((a, b) => scores.get(b) - scores.get(a) || chains.get(b).length - chains.get(a).length || a.localeCompare(b));
|
|
80
|
+
const previous = predecessors[0];
|
|
81
|
+
scores.set(id, (previous ? scores.get(previous) : 0) + (durations.get(id) ?? 0));
|
|
82
|
+
chains.set(id, [...previous ? chains.get(previous) : [], id]);
|
|
83
|
+
}
|
|
84
|
+
const last = [...order].sort((a, b) => scores.get(b) - scores.get(a) || chains.get(b).length - chains.get(a).length || a.localeCompare(b))[0];
|
|
85
|
+
const criticalPath = last ? chains.get(last) : [];
|
|
86
|
+
const cycleLayer = Math.max(-1, ...layer.values()) + 1;
|
|
87
|
+
const nodes = [...tasks.values()].map((task) => ({ id: task.id, title: task.title, owner: task.owner, status: task.status ?? "pending", layer: layer.get(task.id) ?? cycleLayer, ...durations.has(task.id) ? { durationMs: durations.get(task.id) } : {}, critical: criticalPath.includes(task.id) }));
|
|
88
|
+
const layers = [];
|
|
89
|
+
for (const node of nodes) (layers[node.layer] ??= []).push(node.id);
|
|
90
|
+
return { nodes, edges, layers, criticalPath, measuredCriticalPathMs: last ? scores.get(last) : 0, timedTasks: durations.size, completeTiming: durations.size === tasks.size && order.length === tasks.size && [...tasks.values()].every((task) => ["completed", "submitted"].includes(task.status ?? "") && !task.evidenceStale), warnings };
|
|
91
|
+
}
|
|
92
|
+
function analyzeRunTiming(run, steps, now = Date.now()) {
|
|
93
|
+
const end = run.status === "running" ? now : run.endedAt ?? now;
|
|
94
|
+
const elapsedMs = Math.max(0, end - run.createdAt);
|
|
95
|
+
const unique = [...new Map(steps.map((step) => [step.id, step])).values()];
|
|
96
|
+
const records = unique.flatMap((step) => {
|
|
97
|
+
const observed = stepInterval(step, end);
|
|
98
|
+
return observed ? [{ step, observed }] : [];
|
|
99
|
+
});
|
|
100
|
+
const total = (predicate) => records.filter(({ step }) => predicate(step)).reduce((sum, { observed }) => sum + observed[1] - observed[0], 0);
|
|
101
|
+
const occupiedMs = unionMs(records.map(({ observed }) => [Math.max(run.createdAt, observed[0]), Math.min(end, observed[1])]).filter(([start, finish]) => finish >= start));
|
|
102
|
+
const waits = (run.team?.waits ?? []).flatMap((wait) => {
|
|
103
|
+
const observed = interval(wait.createdAt, wait.resolvedAt ?? end);
|
|
104
|
+
return observed ? [[Math.max(run.createdAt, observed[0]), Math.min(end, observed[1])]] : [];
|
|
105
|
+
}).filter(([start, finish]) => finish >= start);
|
|
106
|
+
return { elapsedMs, occupiedMs, unattributedMs: Math.max(0, elapsedMs - occupiedMs), workerMs: total((step) => step.kind === "agent" && !step.coordination && step.role !== "coordinator"), coordinationMs: total((step) => step.kind === "agent" && (step.coordination === true || step.role === "coordinator")), checksMs: total((step) => step.kind !== "agent"), waitingMs: unionMs(waits), timedSteps: records.length, untimedSteps: unique.length - records.length, overlaps: total(() => true) > occupiedMs };
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
analyzeRunTiming,
|
|
110
|
+
analyzeWorkflow
|
|
111
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { constants, closeSync, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, open, rename, rm } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { RUNS_DIR } from "./store.js";
|
|
6
|
+
import { runSearchText } from "./run-history.js";
|
|
7
|
+
const COLD_LIMITS = { indexBytes: 128 * 1024, detailBytes: 16 * 1024 * 1024, cacheBytes: 32 * 1024 * 1024, cacheEntries: 8 };
|
|
8
|
+
const identity = (info) => ({ size: String(info.size), mtimeNs: String(info.mtimeNs), ctimeNs: String(info.ctimeNs), dev: String(info.dev), ino: String(info.ino) });
|
|
9
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
10
|
+
const sha = (text) => createHash("sha256").update(text).digest("hex");
|
|
11
|
+
const validId = (id) => /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,199}$/.test(id);
|
|
12
|
+
const shorten = (text, size) => text && text.length > size ? `${text.slice(0, size)}\u2026` : text;
|
|
13
|
+
function coldEligible(run) {
|
|
14
|
+
return run.archived && run.status === "done" && !run.awaiting && !run.team?.awaitingRequestId && !run.pauseRequested && !run.notes.some((note) => note.status === "queued") && !run.followUps.some((turn) => turn.status === "pending") && Object.keys(run.watches).length === 0 && !run.steps.some((step) => step.status === "running" || step.remote?.ambiguous || step.remote && ["accepted", "running", "unknown"].includes(step.remote.state));
|
|
15
|
+
}
|
|
16
|
+
function coldSummary(run) {
|
|
17
|
+
const { steps: _steps, team: _team, teamRerun: _rerun, verification: _verification, mergeValidation: _mergeValidation, conventions: _conventions, lessons: _lessons, memoryManifest: _memoryManifest, findings: _findings, proofs: _proofs, humanReview: _humanReview, humanRework: _humanRework, patternSummary: _patternSummary, ...metadata } = run;
|
|
18
|
+
return structuredClone({
|
|
19
|
+
...metadata,
|
|
20
|
+
humanRework: run.humanRework?.map((item) => ({ ...item, note: "" })),
|
|
21
|
+
cold: true,
|
|
22
|
+
detailAvailable: true,
|
|
23
|
+
task: shorten(run.task, 8192),
|
|
24
|
+
summary: shorten(run.summary, 8192),
|
|
25
|
+
error: shorten(run.error, 4096),
|
|
26
|
+
notes: [],
|
|
27
|
+
followUps: run.followUps.map((turn) => ({ ...turn, message: "", result: void 0, side: turn.side ? { ...turn.side, title: shorten(turn.side.title, 500) } : void 0 })),
|
|
28
|
+
verification: run.verification ? { version: run.verification.version, state: run.verification.state, profile: run.verification.profile, startedAt: run.verification.startedAt, endedAt: run.verification.endedAt, head: run.verification.head, branch: run.verification.branch, checks: [], skipped: [] } : void 0,
|
|
29
|
+
source: run.source ? { ...run.source, body: "", ci: run.source.ci ? { ...run.source.ci, logs: "" } : void 0 } : void 0,
|
|
30
|
+
externalSource: run.externalSource ? { ...run.externalSource, body: "" } : void 0,
|
|
31
|
+
artifacts: run.artifacts.map((artifact) => ({ ...artifact, stat: artifact.stat?.trim().split("\n").at(-1) })),
|
|
32
|
+
review: run.review ? { ...run.review, summary: "", markdown: "", findings: run.review.findings.map((finding) => ({ severity: finding.severity, raisedBy: finding.raisedBy, file: "", problem: "", fix: "" })) } : void 0,
|
|
33
|
+
integrity: run.integrity ? Object.fromEntries(Object.entries(run.integrity).map(([branch, issues]) => [branch, issues.map((issue) => ({ ...issue, detail: "" }))])) : void 0
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
class RunColdStore {
|
|
37
|
+
constructor(directory = RUNS_DIR) {
|
|
38
|
+
this.directory = directory;
|
|
39
|
+
}
|
|
40
|
+
indexes = /* @__PURE__ */ new Map();
|
|
41
|
+
cache = /* @__PURE__ */ new Map();
|
|
42
|
+
bytes = 0;
|
|
43
|
+
source(id) {
|
|
44
|
+
return join(this.directory, `${id}.jsonl`);
|
|
45
|
+
}
|
|
46
|
+
path(id, kind) {
|
|
47
|
+
if (!validId(id)) throw new Error("Invalid cold run ID");
|
|
48
|
+
return join(this.directory, `${id}.cold-${kind}.json`);
|
|
49
|
+
}
|
|
50
|
+
async regular(path) {
|
|
51
|
+
const info = await lstat(path, { bigint: true });
|
|
52
|
+
if (!info.isFile() || info.isSymbolicLink()) throw new Error("Cold run projections require regular files");
|
|
53
|
+
return identity(info);
|
|
54
|
+
}
|
|
55
|
+
regularSync(path) {
|
|
56
|
+
const info = lstatSync(path, { bigint: true });
|
|
57
|
+
if (!info.isFile() || info.isSymbolicLink()) throw new Error("Cold run projections require regular files");
|
|
58
|
+
return identity(info);
|
|
59
|
+
}
|
|
60
|
+
has(id) {
|
|
61
|
+
return this.indexes.has(id);
|
|
62
|
+
}
|
|
63
|
+
ids() {
|
|
64
|
+
return this.indexes.keys();
|
|
65
|
+
}
|
|
66
|
+
index(id) {
|
|
67
|
+
const item = this.indexes.get(id);
|
|
68
|
+
return item ? structuredClone(item.summary) : void 0;
|
|
69
|
+
}
|
|
70
|
+
search(id) {
|
|
71
|
+
return this.indexes.get(id)?.search;
|
|
72
|
+
}
|
|
73
|
+
summaries() {
|
|
74
|
+
return [...this.indexes.values()].map((item) => structuredClone(item.summary));
|
|
75
|
+
}
|
|
76
|
+
valid(id) {
|
|
77
|
+
const item = this.indexes.get(id);
|
|
78
|
+
if (!item) return false;
|
|
79
|
+
try {
|
|
80
|
+
return same(item.source, this.regularSync(this.source(id))) && same(item.detail, this.regularSync(this.path(id, "detail")));
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
drop(id) {
|
|
86
|
+
this.indexes.delete(id);
|
|
87
|
+
const entry = this.cache.get(id);
|
|
88
|
+
if (entry) this.bytes -= entry.bytes;
|
|
89
|
+
this.cache.delete(id);
|
|
90
|
+
}
|
|
91
|
+
inspect() {
|
|
92
|
+
return { records: this.indexes.size, cached: this.cache.size, bytes: this.bytes, limits: { ...COLD_LIMITS } };
|
|
93
|
+
}
|
|
94
|
+
async load(id) {
|
|
95
|
+
if (!validId(id)) return void 0;
|
|
96
|
+
try {
|
|
97
|
+
const before = await this.regular(this.source(id)), file = this.path(id, "index"), info = await this.regular(file);
|
|
98
|
+
if (Number(info.size) > COLD_LIMITS.indexBytes) return void 0;
|
|
99
|
+
const handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
100
|
+
let text;
|
|
101
|
+
try {
|
|
102
|
+
if ((await handle.stat()).size > COLD_LIMITS.indexBytes) return void 0;
|
|
103
|
+
text = await handle.readFile("utf8");
|
|
104
|
+
} finally {
|
|
105
|
+
await handle.close();
|
|
106
|
+
}
|
|
107
|
+
const envelope = JSON.parse(text), item = envelope.index;
|
|
108
|
+
if (envelope.checksum !== sha(JSON.stringify(item)) || item?.version !== 1 || !Number.isSafeInteger(item.seq) || item.seq < 0 || !item.summary?.cold || !item.summary.detailAvailable || item.summary.id !== id || !item.summary.archived || item.summary.status !== "done" || !/^[a-f0-9]{64}$/.test(item.digest) || !same(item.source, before) || !same(before, await this.regular(this.source(id))) || !same(item.detail, await this.regular(this.path(id, "detail"))) || Number(item.detail.size) > COLD_LIMITS.detailBytes) return void 0;
|
|
109
|
+
this.indexes.set(id, item);
|
|
110
|
+
if (typeof item.search !== "string") {
|
|
111
|
+
const full = await this.getAsync(id);
|
|
112
|
+
if (!full) {
|
|
113
|
+
this.drop(id);
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
item.search = runSearchText(full);
|
|
117
|
+
this.cache.delete(id);
|
|
118
|
+
this.bytes = [...this.cache.values()].reduce((n, entry) => n + entry.bytes, 0);
|
|
119
|
+
}
|
|
120
|
+
return { summary: structuredClone(item.summary), seq: item.seq, search: item.search };
|
|
121
|
+
} catch {
|
|
122
|
+
this.drop(id);
|
|
123
|
+
return void 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async save(run, seq) {
|
|
127
|
+
if (!coldEligible(run) || !validId(run.id)) return false;
|
|
128
|
+
const serialized = JSON.stringify(run);
|
|
129
|
+
if (Buffer.byteLength(serialized) > COLD_LIMITS.detailBytes) return false;
|
|
130
|
+
const summary = coldSummary(run);
|
|
131
|
+
if (Buffer.byteLength(JSON.stringify(summary)) > COLD_LIMITS.indexBytes - 2048) return false;
|
|
132
|
+
const source = await this.regular(this.source(run.id)), detailPath = this.path(run.id, "detail");
|
|
133
|
+
await mkdir(this.directory, { recursive: true });
|
|
134
|
+
const atomic = async (path, text2) => {
|
|
135
|
+
const temp = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
136
|
+
try {
|
|
137
|
+
const file = await open(temp, "wx", 384);
|
|
138
|
+
try {
|
|
139
|
+
await file.writeFile(text2);
|
|
140
|
+
} finally {
|
|
141
|
+
await file.close();
|
|
142
|
+
}
|
|
143
|
+
await rename(temp, path);
|
|
144
|
+
} finally {
|
|
145
|
+
await rm(temp, { force: true });
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
await atomic(detailPath, serialized);
|
|
149
|
+
if (!same(source, await this.regular(this.source(run.id)))) return false;
|
|
150
|
+
const index = { version: 1, source, detail: await this.regular(detailPath), digest: sha(serialized), seq, summary, search: runSearchText(run) };
|
|
151
|
+
const text = JSON.stringify({ index, checksum: sha(JSON.stringify(index)) });
|
|
152
|
+
if (Buffer.byteLength(text) > COLD_LIMITS.indexBytes) return false;
|
|
153
|
+
await atomic(this.path(run.id, "index"), text);
|
|
154
|
+
if (!same(source, await this.regular(this.source(run.id)))) return false;
|
|
155
|
+
this.drop(run.id);
|
|
156
|
+
this.indexes.set(run.id, index);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
async getAsync(id) {
|
|
160
|
+
const index = this.indexes.get(id);
|
|
161
|
+
if (!index) return void 0;
|
|
162
|
+
const valid = async () => same(index.source, await this.regular(this.source(id))) && same(index.detail, await this.regular(this.path(id, "detail")));
|
|
163
|
+
if (!await valid()) return void 0;
|
|
164
|
+
const cached = this.cache.get(id);
|
|
165
|
+
if (cached) {
|
|
166
|
+
this.cache.delete(id);
|
|
167
|
+
this.cache.set(id, cached);
|
|
168
|
+
return structuredClone(cached.run);
|
|
169
|
+
}
|
|
170
|
+
const handle = await open(this.path(id, "detail"), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
171
|
+
let text;
|
|
172
|
+
try {
|
|
173
|
+
const info = await handle.stat();
|
|
174
|
+
if (!info.isFile() || info.size > COLD_LIMITS.detailBytes) throw new Error("Cold run detail exceeds its bound");
|
|
175
|
+
text = await handle.readFile("utf8");
|
|
176
|
+
} finally {
|
|
177
|
+
await handle.close();
|
|
178
|
+
}
|
|
179
|
+
if (Buffer.byteLength(text) > COLD_LIMITS.detailBytes || sha(text) !== index.digest || !await valid() || this.indexes.get(id) !== index) return void 0;
|
|
180
|
+
const run = JSON.parse(text);
|
|
181
|
+
if (run.id !== id || !coldEligible(run)) return void 0;
|
|
182
|
+
this.remember(id, run, Buffer.byteLength(text));
|
|
183
|
+
return structuredClone(run);
|
|
184
|
+
}
|
|
185
|
+
remember(id, run, bytes) {
|
|
186
|
+
const old = this.cache.get(id);
|
|
187
|
+
if (old) {
|
|
188
|
+
this.bytes -= old.bytes;
|
|
189
|
+
this.cache.delete(id);
|
|
190
|
+
}
|
|
191
|
+
while (this.cache.size >= COLD_LIMITS.cacheEntries || this.bytes + bytes > COLD_LIMITS.cacheBytes) {
|
|
192
|
+
const key = this.cache.keys().next().value;
|
|
193
|
+
if (!key) break;
|
|
194
|
+
this.bytes -= this.cache.get(key).bytes;
|
|
195
|
+
this.cache.delete(key);
|
|
196
|
+
}
|
|
197
|
+
this.cache.set(id, { run, bytes });
|
|
198
|
+
this.bytes += bytes;
|
|
199
|
+
}
|
|
200
|
+
/** Sync compatibility for manager.get; detail reads are bounded and cached, never whole logs. */
|
|
201
|
+
get(id) {
|
|
202
|
+
const index = this.indexes.get(id);
|
|
203
|
+
if (!index || !this.valid(id)) return void 0;
|
|
204
|
+
const cached = this.cache.get(id);
|
|
205
|
+
if (cached) {
|
|
206
|
+
this.cache.delete(id);
|
|
207
|
+
this.cache.set(id, cached);
|
|
208
|
+
return structuredClone(cached.run);
|
|
209
|
+
}
|
|
210
|
+
const handle = openSync(this.path(id, "detail"), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
211
|
+
let text;
|
|
212
|
+
try {
|
|
213
|
+
const info = fstatSync(handle);
|
|
214
|
+
if (!info.isFile() || info.size > COLD_LIMITS.detailBytes) throw new Error("Cold run detail exceeds its bound");
|
|
215
|
+
text = readFileSync(handle, "utf8");
|
|
216
|
+
} finally {
|
|
217
|
+
closeSync(handle);
|
|
218
|
+
}
|
|
219
|
+
if (sha(text) !== index.digest || !this.valid(id)) return void 0;
|
|
220
|
+
const run = JSON.parse(text);
|
|
221
|
+
if (run.id !== id || !coldEligible(run)) return void 0;
|
|
222
|
+
this.remember(id, run, Buffer.byteLength(text));
|
|
223
|
+
return structuredClone(run);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
export {
|
|
227
|
+
COLD_LIMITS,
|
|
228
|
+
RunColdStore,
|
|
229
|
+
coldEligible,
|
|
230
|
+
coldSummary
|
|
231
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
function inheritRunEnvironment(requested, parent) {
|
|
3
|
+
const result = { ...requested };
|
|
4
|
+
if (!parent?.permissions) return result;
|
|
5
|
+
for (const key of ["agentContainer", "remoteWorker", "container"]) {
|
|
6
|
+
const inherited = parent.loop[key];
|
|
7
|
+
if (inherited === void 0) continue;
|
|
8
|
+
if (requested[key] !== void 0 && !isDeepStrictEqual(requested[key], inherited)) throw new Error(`Child runs must preserve the parent's ${key} execution boundary. Start an independent run to choose another environment.`);
|
|
9
|
+
result[key] = structuredClone(inherited);
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
inheritRunEnvironment
|
|
15
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
function encodeRunCursor(run) {
|
|
2
|
+
return Buffer.from(JSON.stringify({ at: run.createdAt, id: run.id })).toString("base64url");
|
|
3
|
+
}
|
|
4
|
+
function decodeRunCursor(value) {
|
|
5
|
+
if (!value || value.length > 1e3 || !/^[\w-]+$/.test(value)) throw new Error("Invalid history cursor");
|
|
6
|
+
let cursor;
|
|
7
|
+
try {
|
|
8
|
+
cursor = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
9
|
+
} catch {
|
|
10
|
+
throw new Error("Invalid history cursor");
|
|
11
|
+
}
|
|
12
|
+
if (!cursor || !Number.isSafeInteger(cursor.at) || cursor.at < 0 || typeof cursor.id !== "string" || !cursor.id || cursor.id.length > 256) throw new Error("Invalid history cursor");
|
|
13
|
+
return cursor;
|
|
14
|
+
}
|
|
15
|
+
function compareRuns(a, b) {
|
|
16
|
+
return b.createdAt - a.createdAt || b.id.localeCompare(a.id);
|
|
17
|
+
}
|
|
18
|
+
function afterRunCursor(run, cursor) {
|
|
19
|
+
return compareRuns(run, { createdAt: cursor.at, id: cursor.id }) > 0;
|
|
20
|
+
}
|
|
21
|
+
function runSearchText(run) {
|
|
22
|
+
return [run.task, run.id, run.repo, run.pattern, run.branchPrefix, run.source ? `#${run.source.number} ${run.source.title}` : "", run.externalSource?.title ?? "", run.trigger?.name ?? "", run.group?.label ?? ""].join("\n").toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
class RunIndex {
|
|
25
|
+
records = /* @__PURE__ */ new Map();
|
|
26
|
+
ordered;
|
|
27
|
+
positions = /* @__PURE__ */ new Map();
|
|
28
|
+
repositories = /* @__PURE__ */ new Map();
|
|
29
|
+
revision = 0;
|
|
30
|
+
set(summary, search = runSearchText(summary)) {
|
|
31
|
+
const previous = this.records.get(summary.id)?.summary;
|
|
32
|
+
if (previous) this.removePaths(previous);
|
|
33
|
+
this.records.set(summary.id, { summary, search });
|
|
34
|
+
for (const path of /* @__PURE__ */ new Set([summary.repo, ...summary.worktrees.map((tree) => tree.path)])) {
|
|
35
|
+
let ids = this.repositories.get(path);
|
|
36
|
+
if (!ids) this.repositories.set(path, ids = /* @__PURE__ */ new Set());
|
|
37
|
+
ids.add(summary.id);
|
|
38
|
+
}
|
|
39
|
+
const position = this.positions.get(summary.id);
|
|
40
|
+
if (this.ordered && previous?.createdAt === summary.createdAt && position !== void 0) this.ordered[position] = summary;
|
|
41
|
+
else this.ordered = void 0;
|
|
42
|
+
this.revision++;
|
|
43
|
+
}
|
|
44
|
+
delete(id) {
|
|
45
|
+
const previous = this.records.get(id)?.summary;
|
|
46
|
+
if (!previous) return;
|
|
47
|
+
this.removePaths(previous);
|
|
48
|
+
this.records.delete(id);
|
|
49
|
+
this.ordered = void 0;
|
|
50
|
+
this.revision++;
|
|
51
|
+
}
|
|
52
|
+
removePaths(run) {
|
|
53
|
+
for (const path of [run.repo, ...run.worktrees.map((tree) => tree.path)]) {
|
|
54
|
+
const ids = this.repositories.get(path);
|
|
55
|
+
ids?.delete(run.id);
|
|
56
|
+
if (!ids?.size) this.repositories.delete(path);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
values() {
|
|
60
|
+
if (!this.ordered) {
|
|
61
|
+
this.ordered = [...this.records.values()].map((entry) => entry.summary).sort(compareRuns);
|
|
62
|
+
this.positions = new Map(this.ordered.map((run, index) => [run.id, index]));
|
|
63
|
+
}
|
|
64
|
+
return this.ordered;
|
|
65
|
+
}
|
|
66
|
+
related(repo) {
|
|
67
|
+
return [...this.repositories.get(repo) ?? []].map((id) => this.records.get(id).summary);
|
|
68
|
+
}
|
|
69
|
+
matches(id, words) {
|
|
70
|
+
const text = this.records.get(id)?.search ?? "";
|
|
71
|
+
return words.every((word) => text.includes(word));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function historyOffset(runs, cursor, before) {
|
|
75
|
+
let low = 0, high = runs.length;
|
|
76
|
+
while (low < high) {
|
|
77
|
+
const middle = Math.floor((low + high) / 2), run = runs[middle];
|
|
78
|
+
if ((!cursor || afterRunCursor(run, cursor)) && (before === void 0 || run.createdAt < before)) high = middle;
|
|
79
|
+
else low = middle + 1;
|
|
80
|
+
}
|
|
81
|
+
return low;
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
RunIndex,
|
|
85
|
+
afterRunCursor,
|
|
86
|
+
compareRuns,
|
|
87
|
+
decodeRunCursor,
|
|
88
|
+
encodeRunCursor,
|
|
89
|
+
historyOffset,
|
|
90
|
+
runSearchText
|
|
91
|
+
};
|
package/src/run-log.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
function replayEvents(events) {
|
|
2
|
+
let lastRun = -1;
|
|
3
|
+
const lastStep = /* @__PURE__ */ new Map();
|
|
4
|
+
events.forEach((e, i) => {
|
|
5
|
+
if (e.type === "run") lastRun = i;
|
|
6
|
+
else if (e.type === "step") lastStep.set(e.step.id, i);
|
|
7
|
+
});
|
|
8
|
+
return events.filter((e, i) => e.type === "run" ? i === lastRun : e.type === "step" ? lastStep.get(e.step.id) === i : true);
|
|
9
|
+
}
|
|
10
|
+
const MAX_PATCHES = 100;
|
|
11
|
+
const MIN_STRING_KEPT = 64;
|
|
12
|
+
class RunLogWriter {
|
|
13
|
+
constructor(fullAtEnd = true) {
|
|
14
|
+
this.fullAtEnd = fullAtEnd;
|
|
15
|
+
}
|
|
16
|
+
last = /* @__PURE__ */ new Map();
|
|
17
|
+
/** A line for this run didn't make it to disk: its next snapshot is written whole. */
|
|
18
|
+
forget(runId) {
|
|
19
|
+
this.last.delete(runId);
|
|
20
|
+
}
|
|
21
|
+
encode(event, opts = {}) {
|
|
22
|
+
if (event.type !== "run") return event;
|
|
23
|
+
const json = fieldsOf(event.run);
|
|
24
|
+
const prev = this.last.get(event.runId);
|
|
25
|
+
const ended = this.fullAtEnd && prev?.run.status === "running" && event.run.status !== "running";
|
|
26
|
+
if (!prev || opts.full || ended || prev.patches >= MAX_PATCHES || prev.bytes >= prev.full) {
|
|
27
|
+
this.last.set(event.runId, { seq: event.seq, run: event.run, json, patches: 0, bytes: 0, full: sizeOf(json) });
|
|
28
|
+
return event;
|
|
29
|
+
}
|
|
30
|
+
const patch = { seq: event.seq, ts: event.ts, runId: event.runId, type: "run-patch", base: prev.seq };
|
|
31
|
+
for (const [key, value] of json) {
|
|
32
|
+
const before = prev.json.get(key);
|
|
33
|
+
if (before === value) continue;
|
|
34
|
+
const tail = before === void 0 ? void 0 : tailOf(prev.run[key], event.run[key], value);
|
|
35
|
+
if (tail) (patch.tail ??= {})[key] = tail;
|
|
36
|
+
else (patch.set ??= {})[key] = event.run[key];
|
|
37
|
+
}
|
|
38
|
+
for (const key of prev.json.keys()) if (!json.has(key)) (patch.unset ??= []).push(key);
|
|
39
|
+
this.last.set(event.runId, { ...prev, seq: event.seq, run: event.run, json, patches: prev.patches + 1, bytes: prev.bytes + JSON.stringify(patch).length });
|
|
40
|
+
return patch;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function fieldsOf(run) {
|
|
44
|
+
const json = /* @__PURE__ */ new Map();
|
|
45
|
+
for (const [key, value] of Object.entries(run)) {
|
|
46
|
+
const text = JSON.stringify(value);
|
|
47
|
+
if (text !== void 0) json.set(key, text);
|
|
48
|
+
}
|
|
49
|
+
return json;
|
|
50
|
+
}
|
|
51
|
+
const sizeOf = (json) => [...json].reduce((n, [k, v]) => n + k.length + v.length + 4, 2);
|
|
52
|
+
function tailOf(before, after, afterJson) {
|
|
53
|
+
let kept = 0;
|
|
54
|
+
if (typeof before === "string" && typeof after === "string") {
|
|
55
|
+
const n = Math.min(before.length, after.length);
|
|
56
|
+
while (kept < n && before.charCodeAt(kept) === after.charCodeAt(kept)) kept++;
|
|
57
|
+
if (kept < MIN_STRING_KEPT) return void 0;
|
|
58
|
+
return [kept, after.slice(kept)];
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
61
|
+
const n = Math.min(before.length, after.length);
|
|
62
|
+
while (kept < n && JSON.stringify(before[kept]) === JSON.stringify(after[kept])) kept++;
|
|
63
|
+
if (!kept) return void 0;
|
|
64
|
+
const rest = after.slice(kept);
|
|
65
|
+
return JSON.stringify(rest).length < afterJson.length * 0.8 ? [kept, rest] : void 0;
|
|
66
|
+
}
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
class RunLogReader {
|
|
70
|
+
current;
|
|
71
|
+
currentSeq = -1;
|
|
72
|
+
decode(value) {
|
|
73
|
+
const line = value;
|
|
74
|
+
if (line?.type === "run") {
|
|
75
|
+
this.current = line.run;
|
|
76
|
+
this.currentSeq = line.seq;
|
|
77
|
+
return line;
|
|
78
|
+
} else if (line?.type === "run-patch") {
|
|
79
|
+
if (!this.current || this.current.id !== line.runId || line.base !== this.currentSeq) {
|
|
80
|
+
this.current = void 0;
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
const next = { ...this.current };
|
|
84
|
+
for (const [key, value2] of Object.entries(line.set ?? {})) next[key] = value2;
|
|
85
|
+
for (const [key, [at, rest]] of Object.entries(line.tail ?? {})) {
|
|
86
|
+
const before = next[key];
|
|
87
|
+
if (typeof before === "string" && typeof rest === "string" && before.length >= at) next[key] = before.slice(0, at) + rest;
|
|
88
|
+
else if (Array.isArray(before) && Array.isArray(rest) && before.length >= at) next[key] = [...before.slice(0, at), ...rest];
|
|
89
|
+
}
|
|
90
|
+
for (const key of line.unset ?? []) delete next[key];
|
|
91
|
+
this.current = next;
|
|
92
|
+
this.currentSeq = line.seq;
|
|
93
|
+
return { seq: line.seq, ts: line.ts, runId: line.runId, type: "run", run: this.current };
|
|
94
|
+
} else if (line) {
|
|
95
|
+
return line;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
class StrictRunLogReader {
|
|
100
|
+
constructor(runId) {
|
|
101
|
+
this.runId = runId;
|
|
102
|
+
}
|
|
103
|
+
reader = new RunLogReader();
|
|
104
|
+
current;
|
|
105
|
+
currentSeq;
|
|
106
|
+
decode(value) {
|
|
107
|
+
try {
|
|
108
|
+
if (!record(value) || typeof value.type !== "string" || value.runId !== void 0 && value.runId !== this.runId) throw new Error("Invalid or mismatched run log record");
|
|
109
|
+
if (value.type === "run") {
|
|
110
|
+
if (!record(value.run) || value.run.id !== this.runId || value.seq !== void 0 && !sequence(value.seq)) throw new Error("Invalid or mismatched run snapshot");
|
|
111
|
+
} else if (value.type === "run-patch") {
|
|
112
|
+
if (Object.keys(value).some((key) => !["seq", "ts", "runId", "type", "base", "set", "tail", "unset"].includes(key)) || value.runId !== this.runId || !sequence(value.seq) || !sequence(value.base) || value.seq <= value.base || !this.current || value.base !== this.currentSeq) throw new Error("Invalid or missing run patch base");
|
|
113
|
+
if (value.set !== void 0 && !record(value.set) || value.tail !== void 0 && !record(value.tail) || value.unset !== void 0 && (!Array.isArray(value.unset) || value.unset.some((key) => typeof key !== "string"))) throw new Error("Invalid run patch fields");
|
|
114
|
+
const set = value.set, tail = value.tail, unset = value.unset;
|
|
115
|
+
const keys = [...Object.keys(set ?? {}), ...Object.keys(tail ?? {}), ...unset ?? []];
|
|
116
|
+
if (new Set(keys).size !== keys.length || keys.some((key) => !key || ["id", "__proto__", "constructor", "prototype"].includes(key))) throw new Error("Invalid or overlapping run patch fields");
|
|
117
|
+
for (const [key, change] of Object.entries(tail ?? {})) {
|
|
118
|
+
if (!Array.isArray(change) || change.length !== 2 || !sequence(change[0])) throw new Error("Invalid run patch tail");
|
|
119
|
+
const [at, rest] = change, before = this.current[key];
|
|
120
|
+
if (!(typeof before === "string" && typeof rest === "string" && at <= before.length || Array.isArray(before) && Array.isArray(rest) && at <= before.length)) throw new Error("Invalid run patch tail");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const event = this.reader.decode(value);
|
|
124
|
+
if (event?.type === "run") {
|
|
125
|
+
this.current = event.run;
|
|
126
|
+
this.currentSeq = sequence(value.seq) ? value.seq : void 0;
|
|
127
|
+
}
|
|
128
|
+
return event;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.reader = new RunLogReader();
|
|
131
|
+
this.current = void 0;
|
|
132
|
+
this.currentSeq = void 0;
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const record = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
138
|
+
const sequence = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
139
|
+
function decodeRunLog(lines) {
|
|
140
|
+
const out = [], reader = new RunLogReader();
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
const event = reader.decode(line);
|
|
143
|
+
if (event) out.push(event);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
function compactRunLog(events) {
|
|
148
|
+
const writer = new RunLogWriter(false);
|
|
149
|
+
let lastRun = -1;
|
|
150
|
+
events.forEach((e, i) => e.type === "run" && (lastRun = i));
|
|
151
|
+
return events.map((e, i) => writer.encode(e, { full: i === lastRun }));
|
|
152
|
+
}
|
|
153
|
+
export {
|
|
154
|
+
RunLogReader,
|
|
155
|
+
RunLogWriter,
|
|
156
|
+
StrictRunLogReader,
|
|
157
|
+
compactRunLog,
|
|
158
|
+
decodeRunLog,
|
|
159
|
+
replayEvents
|
|
160
|
+
};
|