opencode-agent-skill 14.4.0 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -15
- package/docs/PI-COMPAT.md +151 -2
- package/docs/V15-MANAGED-RUNTIME.md +128 -0
- package/global-config/agents/architect.md +63 -1
- package/global-config/agents/executor.md +7 -0
- package/global-config/agents/plan-checker.md +8 -1
- package/global-config/agents/verifier.md +3 -3
- package/global-config/skills/database-engineering/references/workflow.md +15 -0
- package/global-config/skills/engineering-orchestrator/references/long-horizon.md +21 -11
- package/lib/activity-deadline.mjs +70 -0
- package/lib/adaptive-context-budget.mjs +25 -5
- package/lib/affected-tests.mjs +1 -1
- package/lib/capability-fabric.mjs +17 -0
- package/lib/completion-auditor.mjs +25 -5
- package/lib/evidence-store.mjs +92 -8
- package/lib/execution-contract.mjs +579 -0
- package/lib/leaf-runtime-optimizer.mjs +76 -0
- package/lib/pi-child-invocation.mjs +65 -0
- package/lib/pi-rpc-pool.mjs +61 -9
- package/lib/plan-salvage.mjs +124 -0
- package/lib/planning-speed-policy.mjs +131 -0
- package/lib/repo-graph.mjs +1 -1
- package/lib/repo-inspect.mjs +1 -0
- package/lib/runtime-artifacts.mjs +42 -0
- package/lib/runtime-events.mjs +33 -1
- package/lib/safety.mjs +1 -0
- package/lib/semantic-index.mjs +1 -1
- package/lib/service-manager.mjs +526 -0
- package/lib/session-display.mjs +49 -0
- package/lib/task-graph.mjs +87 -0
- package/lib/task-policy.mjs +150 -4
- package/lib/trajectory.mjs +111 -1
- package/lib/turbo-fast-path.mjs +44 -0
- package/lib/workspace-fingerprint.mjs +2 -11
- package/lib/workspace-root.mjs +49 -0
- package/lib/worktree-sandbox.mjs +234 -11
- package/package.json +8 -5
- package/pi/extensions/ues-child-runtime.ts +145 -2
- package/pi/extensions/ues.ts +1760 -160
- package/scripts/check-release-consistency.mjs +25 -2
- package/scripts/check-source-integrity.mjs +471 -4
- package/scripts/eval-pi.mjs +76 -36
- package/scripts/smoke-pi-extension.mjs +1 -1
- package/pi/prompts/ues-run.md +0 -17
|
@@ -13,6 +13,11 @@ When the user explicitly chooses the long-horizon workflow (for example with `/u
|
|
|
13
13
|
STATE.json
|
|
14
14
|
EVIDENCE.json
|
|
15
15
|
EVENTS.jsonl
|
|
16
|
+
EXECUTION_CONTRACT.json
|
|
17
|
+
FINAL_VERDICTS.json
|
|
18
|
+
phases/
|
|
19
|
+
MANIFEST.json
|
|
20
|
+
phase-XX-<slug>.json
|
|
16
21
|
tasks/
|
|
17
22
|
reports/
|
|
18
23
|
```
|
|
@@ -21,11 +26,15 @@ The directory is git-ignored execution state, not hidden reasoning. Store requir
|
|
|
21
26
|
|
|
22
27
|
## Machine-enforced gates
|
|
23
28
|
|
|
24
|
-
The
|
|
29
|
+
The runtime enforces seven boundaries:
|
|
25
30
|
|
|
26
|
-
1. **
|
|
27
|
-
2. **
|
|
28
|
-
3. **
|
|
31
|
+
1. **Inherited-work gate** — snapshot source-facing dirty paths before planning. UES children may not discard them with restore/checkout/stash/clean/reset, and planned writers must declare every touched file.
|
|
32
|
+
2. **Local-env gate** — local `.env*` runtime inputs are no-write by default; only explicit user authorization permits local env mutation. Repository templates remain writable.
|
|
33
|
+
3. **Plan gate** — `work plan` leaves the item in `awaiting-plan-approval`. Long/high-risk plans require a structured `plan-verification` receipt bound to the current plan hash before `work approve-plan` succeeds.
|
|
34
|
+
4. **Phase gate** — explicit execution phases become deterministic previous-phase barriers. Constraint-only phases stay global invariants.
|
|
35
|
+
5. **Concurrent state gate** — all mutable `STATE.json` and `EVIDENCE.json` operations use a per-work-item lock plus atomic file replacement, preventing safe-wave executors from losing each other's state.
|
|
36
|
+
6. **Integration gate** — long/high-risk source completion requires a structured `integration-verification` receipt bound to the current workspace fingerprint. Finalization rejects any later workspace change.
|
|
37
|
+
7. **Verdict matrix gate** — source, runtime, database cleanup and real-device proof remain independent. Missing required evidence is reported as not verified rather than promoted to PASS.
|
|
29
38
|
|
|
30
39
|
## Pipeline
|
|
31
40
|
|
|
@@ -33,18 +42,19 @@ The V6 engine enforces three boundaries:
|
|
|
33
42
|
2. Write observable acceptance criteria into `SPEC.md`.
|
|
34
43
|
3. Initialize state with `ocskill work init`.
|
|
35
44
|
4. Create `PLAN.json` following the plan schema and import it with `ocskill work plan`.
|
|
36
|
-
5.
|
|
37
|
-
6.
|
|
38
|
-
7.
|
|
45
|
+
5. If the user supplied explicit `PHASE N` sections, preserve every execution phase in the plan and let UES add phase barriers; constraint-only phases remain invariants.
|
|
46
|
+
6. Run `ues-plan-checker`. If it returns PASS, create `work gate-receipt <slug> plan` and persist approval with `work approve-plan --receipt-file ...`.
|
|
47
|
+
7. Persist the execution contract/phase artifacts and use `ocskill task-graph` to compute dependency-safe waves.
|
|
48
|
+
8. For each ready task:
|
|
39
49
|
- on OpenCode V2, if two or more approved tasks are independent, prefer `ues.dispatch_parallel` so one shared model can execute them concurrently with isolated worktrees, leases, independent verifier sessions and serialized integration;
|
|
40
50
|
- otherwise prefer `ues.dispatch_task`, which performs `work start`, creates a fresh `ues-executor` session, selects the configured attempt-based model tier, prompts it with a bounded context pack and waits for completion;
|
|
41
51
|
- inspect the child diff and verification;
|
|
42
52
|
- record at least one successful `work verify-command` receipt for long/high-risk work;
|
|
43
53
|
- persist `work complete --evidence ...` or `work fail --reason ...`.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
9. On failure, re-diagnose rather than stacking patches. A later `ues.dispatch_task` attempt can escalate from standard to heavy when configured.
|
|
55
|
+
10. After all tasks complete, run `ues-integration-verifier`.
|
|
56
|
+
11. Persist the verifier's actual result with a structured integration receipt plus `ocskill work verify-integration --receipt-file ...`.
|
|
57
|
+
12. Emit the independent final verdict matrix and persist `FINAL_VERDICTS.json`. Only a recorded PASS with an unchanged workspace can be finalized.
|
|
48
58
|
|
|
49
59
|
## Parallelism
|
|
50
60
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
function finite(value, fallback, min = 0, max = Number.MAX_SAFE_INTEGER) {
|
|
2
|
+
const parsed = Number(value)
|
|
3
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
4
|
+
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createAdaptiveDeadline(options = {}, startedAt = Date.now()) {
|
|
8
|
+
const initialMs = finite(options.hardTimeoutMs, 60_000, 1_000)
|
|
9
|
+
const absoluteMs = Math.max(
|
|
10
|
+
initialMs,
|
|
11
|
+
finite(options.absoluteHardTimeoutMs, initialMs, initialMs),
|
|
12
|
+
)
|
|
13
|
+
const extensionMs = finite(options.activityExtensionMs, 0, 0, absoluteMs)
|
|
14
|
+
const activityWindowMs = finite(
|
|
15
|
+
options.activityWindowMs,
|
|
16
|
+
Math.max(5_000, Math.min(extensionMs || initialMs, 30_000)),
|
|
17
|
+
1_000,
|
|
18
|
+
absoluteMs,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
let deadlineAt = startedAt + initialMs
|
|
22
|
+
const absoluteAt = startedAt + absoluteMs
|
|
23
|
+
let extensions = 0
|
|
24
|
+
|
|
25
|
+
function maybeExtend(now = Date.now(), lastActivityAt = 0) {
|
|
26
|
+
if (extensionMs <= 0 || deadlineAt >= absoluteAt) {
|
|
27
|
+
return { extended: false, deadlineAt, absoluteAt, extensions }
|
|
28
|
+
}
|
|
29
|
+
if (now - Number(lastActivityAt || 0) > activityWindowMs) {
|
|
30
|
+
return { extended: false, deadlineAt, absoluteAt, extensions }
|
|
31
|
+
}
|
|
32
|
+
const next = Math.min(absoluteAt, Math.max(deadlineAt, now + extensionMs))
|
|
33
|
+
if (next <= deadlineAt) {
|
|
34
|
+
return { extended: false, deadlineAt, absoluteAt, extensions }
|
|
35
|
+
}
|
|
36
|
+
deadlineAt = next
|
|
37
|
+
extensions += 1
|
|
38
|
+
return { extended: true, deadlineAt, absoluteAt, extensions }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function shouldAbort(now = Date.now(), lastActivityAt = 0) {
|
|
42
|
+
if (now < deadlineAt) {
|
|
43
|
+
return { abort: false, deadlineAt, absoluteAt, extensions }
|
|
44
|
+
}
|
|
45
|
+
const extension = maybeExtend(now, lastActivityAt)
|
|
46
|
+
if (extension.extended) {
|
|
47
|
+
return { abort: false, ...extension }
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
abort: true,
|
|
51
|
+
reason: now >= absoluteAt ? "absolute-hard-timeout" : "hard-timeout",
|
|
52
|
+
deadlineAt,
|
|
53
|
+
absoluteAt,
|
|
54
|
+
extensions,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
startedAt,
|
|
60
|
+
initialMs,
|
|
61
|
+
absoluteMs,
|
|
62
|
+
extensionMs,
|
|
63
|
+
activityWindowMs,
|
|
64
|
+
get deadlineAt() { return deadlineAt },
|
|
65
|
+
get absoluteAt() { return absoluteAt },
|
|
66
|
+
get extensions() { return extensions },
|
|
67
|
+
maybeExtend,
|
|
68
|
+
shouldAbort,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -59,20 +59,35 @@ export function adaptiveContextBudget(taskPolicy = {}, role = "executor", attemp
|
|
|
59
59
|
const profile = String(taskPolicy.executionProfile || taskPolicy.profile?.name || "standard")
|
|
60
60
|
const highRisk = taskPolicy.risk === "high" || taskPolicy.risk === "critical"
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
// away security/payment/schema evidence for latency.
|
|
64
|
-
if (highRisk || options.disabled === true) {
|
|
62
|
+
if (options.disabled === true) {
|
|
65
63
|
return {
|
|
66
64
|
schemaVersion: 1,
|
|
67
65
|
budget: base,
|
|
68
66
|
baseBudget: base,
|
|
69
67
|
adaptive: false,
|
|
70
|
-
reason:
|
|
68
|
+
reason: "disabled",
|
|
71
69
|
}
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
const table = ROLE_TARGETS[profile] || ROLE_TARGETS.standard
|
|
75
73
|
const target = clamp(table?.[role], base, 4_000, base)
|
|
74
|
+
const plannerRole = role === "architect" || role === "plan-checker"
|
|
75
|
+
|
|
76
|
+
// High-risk execution/verification keeps the full evidence ceiling. Read-only
|
|
77
|
+
// planning roles are allowed a bounded context because they can use targeted
|
|
78
|
+
// repository tools and are independently plan-gated before any write begins.
|
|
79
|
+
if (highRisk && !plannerRole) {
|
|
80
|
+
return {
|
|
81
|
+
schemaVersion: 1,
|
|
82
|
+
budget: base,
|
|
83
|
+
baseBudget: base,
|
|
84
|
+
adaptive: false,
|
|
85
|
+
reason: "high-risk-execution-preserves-base-budget",
|
|
86
|
+
profile,
|
|
87
|
+
role,
|
|
88
|
+
attempt: Math.max(1, Math.trunc(Number(attempt || 1))),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
76
91
|
const normalizedAttempt = Math.max(1, Math.trunc(Number(attempt || 1)))
|
|
77
92
|
|
|
78
93
|
// Failed attempts expand deterministically back toward the policy ceiling.
|
|
@@ -92,6 +107,11 @@ export function adaptiveContextBudget(taskPolicy = {}, role = "executor", attemp
|
|
|
92
107
|
profile,
|
|
93
108
|
role,
|
|
94
109
|
attempt: normalizedAttempt,
|
|
95
|
-
reason:
|
|
110
|
+
reason:
|
|
111
|
+
highRisk && plannerRole
|
|
112
|
+
? "high-risk-planner-role-bounded"
|
|
113
|
+
: budget < base
|
|
114
|
+
? "role-bounded-expand-on-failure"
|
|
115
|
+
: "policy-ceiling",
|
|
96
116
|
}
|
|
97
117
|
}
|
package/lib/affected-tests.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { runtimeWorkspaceFingerprint } from "./workspace-fingerprint.mjs"
|
|
|
6
6
|
|
|
7
7
|
const TEST_RE = /(?:^|[/.\\])(?:__tests__|tests?|spec)(?:[/.\\]|$)|\.(?:test|spec|e2e-spec)\.[cm]?[jt]sx?$/i
|
|
8
8
|
const SOURCE_EXT = new Set([".js",".jsx",".mjs",".cjs",".ts",".tsx",".py",".java",".kt",".kts",".cs",".go",".rs",".dart",".swift"])
|
|
9
|
-
const SKIP = new Set([".git","node_modules",".next","dist","build","coverage",".venv","venv","target","Pods","DerivedData",".gradle",".ues-cache",".ues-work",".ues-sandboxes",".ues-traces",".ues-learning",".ues-dashboard",".ues-memory",".ues-evals"])
|
|
9
|
+
const SKIP = new Set([".git","node_modules",".next","dist","build","coverage",".venv","venv","target","Pods","DerivedData",".gradle",".ues-cache",".ues-work",".ues-sandboxes",".ues-traces",".ues-learning",".ues-dashboard",".ues-memory",".ues-evals",".ues-services"])
|
|
10
10
|
const AFFECTED_TEST_CACHE = new Map()
|
|
11
11
|
const AFFECTED_TEST_INFLIGHT = new Map()
|
|
12
12
|
|
|
@@ -129,6 +129,23 @@ export function defaultCapabilityRegistry(root = process.cwd()) {
|
|
|
129
129
|
"context.reversible": [
|
|
130
130
|
{ id: "ues-evidence-context", kind: "builtin", priority: 120, quality: 0.98, costClass: "low", latencyClass: "fast", metadata: { reversible: true, searchable: true, levels: ["T1", "T2", "T3", "raw"] } },
|
|
131
131
|
],
|
|
132
|
+
"runtime.service": [
|
|
133
|
+
{
|
|
134
|
+
id: "ues-managed-service",
|
|
135
|
+
kind: "builtin",
|
|
136
|
+
priority: 125,
|
|
137
|
+
quality: 0.98,
|
|
138
|
+
costClass: "low",
|
|
139
|
+
latencyClass: "fast",
|
|
140
|
+
metadata: {
|
|
141
|
+
shellFree: true,
|
|
142
|
+
readiness: ["port", "log", "process"],
|
|
143
|
+
actions: ["start", "wait-ready", "status", "logs", "stop", "restart"],
|
|
144
|
+
boundedLifetime: true,
|
|
145
|
+
evidenceLogs: true,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
],
|
|
132
149
|
memory: [
|
|
133
150
|
{ id: "ues-memory", kind: "builtin", priority: 100, quality: 0.9, costClass: "low", latencyClass: "fast" },
|
|
134
151
|
],
|
|
@@ -11,13 +11,30 @@ function passRun(run) {
|
|
|
11
11
|
return Boolean(run && Number(run.exitCode) === 0 && run.verdict === "PASS")
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
|
|
14
|
+
function normalizeSection(value) {
|
|
15
|
+
return text(value)
|
|
16
16
|
.toLowerCase()
|
|
17
17
|
.replace(/[`*_#]/g, "")
|
|
18
18
|
.replace(/[.!:;]+$/g, "")
|
|
19
|
+
.replace(/\s+/g, " ")
|
|
19
20
|
.trim()
|
|
20
|
-
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function benignEmptySection(value) {
|
|
24
|
+
const normalized = normalizeSection(value)
|
|
25
|
+
return !normalized || /^(?:none(?: observed| found| remaining)?|n\/a|na|nil|0|no failures?(?: observed| found| remaining)?|no unresolved gaps?(?: observed| found| remaining)?|no gaps?(?: observed| found| remaining)?|not applicable|không có|không có gì|không có lỗi|không phát hiện lỗi|không còn khoảng trống|không có khoảng trống)$/.test(normalized)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function benignFailureSection(value) {
|
|
29
|
+
const normalized = normalizeSection(value)
|
|
30
|
+
return benignEmptySection(normalized) ||
|
|
31
|
+
/^(?:không có|không phát hiện) .{0,120}(?:lỗi|thất bại)(?: nào)?$/.test(normalized)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function outOfScopeGapOnly(value) {
|
|
35
|
+
const normalized = normalizeSection(value)
|
|
36
|
+
if (benignEmptySection(normalized)) return true
|
|
37
|
+
return /(?:not requested|outside (?:the )?scope|out of scope|not required|không có yêu cầu|không được yêu cầu|ngoài phạm vi|không thuộc phạm vi)/.test(normalized)
|
|
21
38
|
}
|
|
22
39
|
|
|
23
40
|
function receiptPassed(row = {}) {
|
|
@@ -43,8 +60,11 @@ export function auditCompletion(input = {}) {
|
|
|
43
60
|
for (const key of ["checks-run", "acceptance-criteria-proven", "completion-evidence"]) {
|
|
44
61
|
if (!text(sections[key])) failures.push(`missing-report-section:${key}`)
|
|
45
62
|
}
|
|
46
|
-
if (Object.hasOwn(sections, "failures") && !
|
|
47
|
-
if (Object.hasOwn(sections, "unresolved-gaps") && !benignEmptySection(sections["unresolved-gaps"]))
|
|
63
|
+
if (Object.hasOwn(sections, "failures") && !benignFailureSection(sections.failures)) failures.push("verification-reports-failures")
|
|
64
|
+
if (Object.hasOwn(sections, "unresolved-gaps") && !benignEmptySection(sections["unresolved-gaps"])) {
|
|
65
|
+
if (outOfScopeGapOnly(sections["unresolved-gaps"])) warnings.push("verification-reports-out-of-scope-gap")
|
|
66
|
+
else failures.push("verification-reports-unresolved-gaps")
|
|
67
|
+
}
|
|
48
68
|
if (Object.hasOwn(sections, "checks-not-run") && !benignEmptySection(sections["checks-not-run"])) warnings.push("verification-reports-checks-not-run")
|
|
49
69
|
|
|
50
70
|
const snapshot = input.workspaceSnapshot || null
|
package/lib/evidence-store.mjs
CHANGED
|
@@ -5,6 +5,41 @@ import path from "node:path"
|
|
|
5
5
|
|
|
6
6
|
const STORE_VERSION = 1
|
|
7
7
|
const REF_PREFIX = "evidence:sha256:"
|
|
8
|
+
const DEFAULT_MAX_STORE_BYTES = 192 * 1024 * 1024
|
|
9
|
+
const DEFAULT_MAX_STORE_ENTRIES = 1200
|
|
10
|
+
const DEFAULT_MAX_STORE_AGE_DAYS = 14
|
|
11
|
+
const AUTO_GC_EVERY_WRITES = 8
|
|
12
|
+
const AUTO_GC_LARGE_ITEM_BYTES = 16 * 1024 * 1024
|
|
13
|
+
const PUT_COUNTS = new Map()
|
|
14
|
+
|
|
15
|
+
function boundedNumber(value, fallback, min, max) {
|
|
16
|
+
const parsed = Number(value)
|
|
17
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
18
|
+
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function defaultStoreLimits() {
|
|
22
|
+
return {
|
|
23
|
+
maxBytes: boundedNumber(
|
|
24
|
+
process.env.UES_EVIDENCE_MAX_BYTES,
|
|
25
|
+
DEFAULT_MAX_STORE_BYTES,
|
|
26
|
+
16 * 1024 * 1024,
|
|
27
|
+
2 * 1024 * 1024 * 1024,
|
|
28
|
+
),
|
|
29
|
+
maxEntries: boundedNumber(
|
|
30
|
+
process.env.UES_EVIDENCE_MAX_ENTRIES,
|
|
31
|
+
DEFAULT_MAX_STORE_ENTRIES,
|
|
32
|
+
100,
|
|
33
|
+
20_000,
|
|
34
|
+
),
|
|
35
|
+
maxAgeDays: boundedNumber(
|
|
36
|
+
process.env.UES_EVIDENCE_MAX_AGE_DAYS,
|
|
37
|
+
DEFAULT_MAX_STORE_AGE_DAYS,
|
|
38
|
+
1,
|
|
39
|
+
365,
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
8
43
|
|
|
9
44
|
function stableValue(value) {
|
|
10
45
|
if (Array.isArray(value)) return value.map(stableValue)
|
|
@@ -21,6 +56,24 @@ function toBytes(value, options = {}) {
|
|
|
21
56
|
return { bytes: Buffer.from(text, "utf8"), encoding: "utf8", mediaType: options.mediaType || "application/json" }
|
|
22
57
|
}
|
|
23
58
|
|
|
59
|
+
async function protectedEvidenceHashes(root) {
|
|
60
|
+
const file = path.join(path.resolve(root), ".ues-memory", "MEMORY.json")
|
|
61
|
+
const parsed = await readFile(file, "utf8")
|
|
62
|
+
.then((raw) => JSON.parse(raw))
|
|
63
|
+
.catch(() => null)
|
|
64
|
+
const protectedHashes = new Set()
|
|
65
|
+
const now = Date.now()
|
|
66
|
+
for (const memory of parsed?.memories || []) {
|
|
67
|
+
if (memory?.status !== "verified" || memory?.supersededBy) continue
|
|
68
|
+
const expiresAt = Date.parse(String(memory?.expiresAt || ""))
|
|
69
|
+
if (expiresAt && expiresAt <= now) continue
|
|
70
|
+
for (const ref of memory?.evidenceRefs || []) {
|
|
71
|
+
try { protectedHashes.add(normalizeHash(ref)) } catch {}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return protectedHashes
|
|
75
|
+
}
|
|
76
|
+
|
|
24
77
|
function normalizeHash(ref) {
|
|
25
78
|
const value = String(ref || "").trim()
|
|
26
79
|
const hash = value.startsWith(REF_PREFIX) ? value.slice(REF_PREFIX.length) : value.replace(/^sha256:/, "")
|
|
@@ -79,6 +132,13 @@ export async function putEvidence(root, value, options = {}) {
|
|
|
79
132
|
preview,
|
|
80
133
|
}
|
|
81
134
|
await writeFile(target.meta, JSON.stringify(metadata, null, 2) + "\n", "utf8")
|
|
135
|
+
|
|
136
|
+
const writes = (PUT_COUNTS.get(root) || 0) + 1
|
|
137
|
+
PUT_COUNTS.set(root, writes)
|
|
138
|
+
if (writes % AUTO_GC_EVERY_WRITES === 0 || encoded.bytes.length >= AUTO_GC_LARGE_ITEM_BYTES) {
|
|
139
|
+
await gcEvidenceStore(root, defaultStoreLimits()).catch(() => null)
|
|
140
|
+
}
|
|
141
|
+
|
|
82
142
|
return metadata
|
|
83
143
|
}
|
|
84
144
|
|
|
@@ -216,25 +276,44 @@ export async function evidenceStoreStatus(root = process.cwd()) {
|
|
|
216
276
|
}
|
|
217
277
|
|
|
218
278
|
export async function gcEvidenceStore(root = process.cwd(), options = {}) {
|
|
279
|
+
root = path.resolve(root)
|
|
219
280
|
const files = await metadataFiles(root)
|
|
220
|
-
const
|
|
221
|
-
const
|
|
281
|
+
const defaults = defaultStoreLimits()
|
|
282
|
+
const maxEntries = Math.max(10, Number(options.maxEntries ?? defaults.maxEntries))
|
|
283
|
+
const maxBytes = Math.max(1024 * 1024, Number(options.maxBytes ?? defaults.maxBytes))
|
|
284
|
+
const maxAgeMs = Math.max(0, Number(options.maxAgeDays ?? defaults.maxAgeDays)) * 86_400_000
|
|
222
285
|
const now = Date.now()
|
|
286
|
+
const protectedHashes = options.protectMemoryRefs === false
|
|
287
|
+
? new Set()
|
|
288
|
+
: await protectedEvidenceHashes(root)
|
|
223
289
|
const rows = []
|
|
224
290
|
for (const metaFile of files) {
|
|
225
291
|
const meta = JSON.parse(await readFile(metaFile, "utf8").catch(() => "{}"))
|
|
226
292
|
const time = Date.parse(meta.lastSeenAt || meta.createdAt || 0) || 0
|
|
227
|
-
rows.push({ metaFile, meta, time })
|
|
293
|
+
rows.push({ metaFile, meta, time, bytes: Math.max(0, Number(meta.bytes || 0)) })
|
|
228
294
|
}
|
|
229
295
|
rows.sort((a, b) => b.time - a.time)
|
|
230
296
|
|
|
231
297
|
const removed = []
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
298
|
+
let retainedEntries = 0
|
|
299
|
+
let retainedBytes = 0
|
|
300
|
+
let protectedEntries = 0
|
|
301
|
+
let protectedBytes = 0
|
|
302
|
+
for (const row of rows) {
|
|
303
|
+
const hash = normalizeHash(row.meta.sha256 || path.basename(row.metaFile, ".json"))
|
|
304
|
+
const protectedByMemory = protectedHashes.has(hash)
|
|
305
|
+
const tooMany = retainedEntries >= maxEntries
|
|
235
306
|
const tooOld = maxAgeMs > 0 && row.time > 0 && now - row.time > maxAgeMs
|
|
236
|
-
|
|
237
|
-
|
|
307
|
+
const tooLarge = retainedBytes + row.bytes > maxBytes
|
|
308
|
+
if (protectedByMemory || (!tooMany && !tooOld && !tooLarge)) {
|
|
309
|
+
retainedEntries += 1
|
|
310
|
+
retainedBytes += row.bytes
|
|
311
|
+
if (protectedByMemory) {
|
|
312
|
+
protectedEntries += 1
|
|
313
|
+
protectedBytes += row.bytes
|
|
314
|
+
}
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
238
317
|
const target = evidencePaths(root, hash)
|
|
239
318
|
await rm(target.meta, { force: true })
|
|
240
319
|
await rm(target.data, { force: true })
|
|
@@ -244,6 +323,11 @@ export async function gcEvidenceStore(root = process.cwd(), options = {}) {
|
|
|
244
323
|
return {
|
|
245
324
|
removed,
|
|
246
325
|
removedCount: removed.length,
|
|
326
|
+
retainedEntries,
|
|
327
|
+
retainedBytes,
|
|
328
|
+
protectedEntries,
|
|
329
|
+
protectedBytes,
|
|
330
|
+
limits: { maxEntries, maxBytes, maxAgeMs },
|
|
247
331
|
status: await evidenceStoreStatus(root),
|
|
248
332
|
}
|
|
249
333
|
}
|