fidelis 0.1.0 → 0.1.1

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/dist/auditor.d.ts CHANGED
@@ -32,6 +32,8 @@ export declare class Auditor {
32
32
  }): boolean;
33
33
  /** A turn that failed or was interrupted has no completed boundary to audit: nothing is running any more. */
34
34
  executionEnded(sessionID: string): void;
35
+ /** The agent never received the correction, so nothing is being corrected. */
36
+ deliveryFailed(sessionID: string, reason: string): void;
35
37
  /** Merge host events with the ledger so compaction cannot erase evidence the audit still needs. */
36
38
  retain(sessionID: string, events: ExecEvent[]): ExecEvent[];
37
39
  complete(sessionID: string, input: CompletionInput): Promise<Outcome>;
package/dist/auditor.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { consentProblem } from "./config.js";
3
3
  import { getChecklist, resolveSources } from "./compiler.js";
4
- import { judge } from "./judge.js";
4
+ import { judge, LEDGER_MARK } from "./judge.js";
5
5
  import { transition } from "./machine.js";
6
6
  import { redact } from "./redact.js";
7
7
  import { canonical, sha256 } from "./store.js";
@@ -69,6 +69,15 @@ export class Auditor {
69
69
  state.state = "READY";
70
70
  }, "ended");
71
71
  }
72
+ /** The agent never received the correction, so nothing is being corrected. */
73
+ deliveryFailed(sessionID, reason) {
74
+ this.save(sessionID, (state) => {
75
+ if (state.state !== "CORRECTING")
76
+ return;
77
+ state.state = "AUDIT_INCOMPLETE";
78
+ state.reason = `The correction could not be delivered: ${reason}`;
79
+ }, "incomplete");
80
+ }
72
81
  /** Merge host events with the ledger so compaction cannot erase evidence the audit still needs. */
73
82
  retain(sessionID, events) {
74
83
  const store = this.o.store;
@@ -77,8 +86,13 @@ export class Auditor {
77
86
  for (const event of events)
78
87
  merged.set(event.id, event);
79
88
  const ordered = [...merged.values()].sort((a, b) => (a.at ?? 0) - (b.at ?? 0) || a.seq - b.seq).map((event, seq) => ({ ...event, seq }));
80
- const clipField = (value) => (value === undefined ? undefined : redact(raw ? value : value.slice(0, LEDGER_CLIP)));
81
- store.writeLedger(sessionID, ordered.slice(-LEDGER_MAX).map((event) => ({ ...event, text: clipField(event.text), output: clipField(event.output), input: event.input === undefined ? undefined : JSON.parse(redact(JSON.stringify(event.input))) })));
89
+ const clipField = (value) => {
90
+ if (value === undefined)
91
+ return undefined;
92
+ const clean = redact(value);
93
+ return raw || clean.length <= LEDGER_CLIP ? clean : clean.slice(0, LEDGER_CLIP) + LEDGER_MARK;
94
+ };
95
+ store.writeLedger(sessionID, ordered.slice(-LEDGER_MAX).map((event) => ({ ...event, text: clipField(event.text), output: clipField(event.output), input: clipField(typeof event.input === "string" || event.input === undefined ? event.input : JSON.stringify(event.input)) })));
82
96
  return ordered;
83
97
  }
84
98
  async complete(sessionID, input) {
@@ -110,6 +124,11 @@ export class Auditor {
110
124
  }, "auditing");
111
125
  if (skip)
112
126
  return skip;
127
+ // Spend is saved as it happens, so a superseded audit still counts against the session.
128
+ const charge = (costUSD) => {
129
+ if (costUSD)
130
+ this.save(sessionID, (state) => void (state.spentUSD += costUSD), "usage");
131
+ };
113
132
  const metrics = { cacheHit: false, compilerCalls: 0, judgeCalls: 0, tokens: 0, costUSD: 0, latencyMs: 0, uncertain: 0 };
114
133
  const finish = (event, reason, findings, extra = {}) => {
115
134
  metrics.latencyMs = Date.now() - started;
@@ -127,7 +146,6 @@ export class Auditor {
127
146
  state.findings = findings;
128
147
  state.lastAuditID = auditID;
129
148
  state.boundaries = [...state.boundaries, boundaryID].slice(-MAX_TRACKED);
130
- state.spentUSD += metrics.costUSD;
131
149
  }, event);
132
150
  if (!current)
133
151
  return null;
@@ -167,6 +185,7 @@ export class Auditor {
167
185
  metrics.compilerCalls = compiled.calls;
168
186
  metrics.tokens = compiled.tokens;
169
187
  metrics.costUSD = compiled.costUSD;
188
+ charge(compiled.costUSD);
170
189
  if (!compiled.checklist)
171
190
  return incomplete(compileConsent && !compiled.cacheHit ? compileConsent : (compiled.reason ?? "Checklist unavailable."));
172
191
  this.save(sessionID, (state) => void (state.phase = "judging"), "judging");
@@ -177,18 +196,33 @@ export class Auditor {
177
196
  return incomplete("No decision model is available.");
178
197
  let verdict;
179
198
  try {
180
- verdict = await judge({ checklist: compiled.checklist, evidence: input.evidence, prior: begin.findings.filter((finding) => finding.status === "VIOLATED"), model: this.o.judge, config });
199
+ verdict = await judge({
200
+ checklist: compiled.checklist,
201
+ evidence: input.evidence,
202
+ prior: begin.findings.filter((finding) => finding.status === "VIOLATED"),
203
+ model: this.o.judge,
204
+ config,
205
+ stopReason: () => {
206
+ const current = store.read(sessionID);
207
+ if (current.audit?.id !== auditID)
208
+ return "the audit was superseded by a newer turn";
209
+ return config.maxAuditCostUSD !== null && current.spentUSD >= config.maxAuditCostUSD ? "the audit budget is exhausted" : null;
210
+ },
211
+ onUsage: (tokens, costUSD) => {
212
+ metrics.tokens += tokens;
213
+ metrics.costUSD += costUSD;
214
+ charge(costUSD);
215
+ },
216
+ });
181
217
  }
182
218
  catch (error) {
183
219
  return incomplete(`Judge failed: ${error.message}`);
184
220
  }
185
221
  metrics.judgeCalls = verdict.calls;
186
- metrics.tokens += verdict.tokens;
187
- metrics.costUSD += verdict.costUSD;
188
- return this.route(verdict.findings, verdict.waiting, begin, auditID, finish, stale, incomplete);
222
+ return this.route(verdict.findings, verdict.waiting, compiled.checklist.dropped.length, begin, auditID, finish, stale, incomplete);
189
223
  }
190
224
  /** Deterministic routing. Model output only suggests; these branches decide. */
191
- route(findings, waiting, begin, auditID, finish, stale, incomplete) {
225
+ route(findings, waiting, dropped, begin, auditID, finish, stale, incomplete) {
192
226
  const violated = findings.filter((finding) => finding.status === "VIOLATED");
193
227
  const open = violated.filter((finding) => !finding.resolved);
194
228
  const forUser = open.filter((finding) => finding.repair === "user");
@@ -219,6 +253,9 @@ export class Auditor {
219
253
  const unknown = findings.filter((finding) => finding.status === "UNKNOWN");
220
254
  if (unknown.length)
221
255
  return incomplete(`${unknown.length} rule(s) could not be assessed.`, findings);
256
+ // A rule the compiler produced but validation rejected was never judged, so this cannot be a clean pass.
257
+ if (dropped)
258
+ return incomplete(`${dropped} compiled rule(s) failed validation and were not assessed.`, findings);
222
259
  const reason = violated.length ? `${violated.length} violation(s) on record, repaired or acknowledged after correction.` : undefined;
223
260
  const state = finish("clean", reason, findings);
224
261
  return state ? { action: "none", state, reason, notice: violated.length ? `Fidelis findings:\n${userNotice(violated)}` : undefined } : stale();
package/dist/compiler.js CHANGED
@@ -107,7 +107,7 @@ export async function getChecklist(input) {
107
107
  const fingerprint = checklistFingerprint(host, sources, config, prompt);
108
108
  const cached = config.cacheChecklist ? store.getChecklist(fingerprint) : undefined;
109
109
  const miss = { cacheHit: false, calls: 1, tokens: 0, costUSD: 0 };
110
- if (cached?.schema === SCHEMA_VERSION)
110
+ if (cached?.schema === SCHEMA_VERSION && cached.dropped.length === 0)
111
111
  return { checklist: cached, cacheHit: true, calls: 0, tokens: 0, costUSD: 0 };
112
112
  if (!generate)
113
113
  return { ...miss, calls: 0, reason: "No compiler model is available." };
@@ -118,14 +118,17 @@ export async function getChecklist(input) {
118
118
  miss.tokens = output.tokens ?? 0;
119
119
  miss.costUSD = output.costUSD ?? 0;
120
120
  const raw = parseJson(output.text);
121
- const confidence = typeof raw?.confidence === "number" ? raw.confidence : 1;
121
+ const confidence = raw?.confidence;
122
+ if (typeof confidence !== "number" || !(confidence >= 0 && confidence <= 1))
123
+ return { ...miss, reason: "Compiler output has no valid confidence." };
122
124
  const { rules, dropped } = validateRules(raw, sources);
123
125
  if (confidence < MIN_CONFIDENCE)
124
126
  return { ...miss, reason: `Compiler confidence ${confidence} is too low.` };
125
127
  if (rules.length === 0)
126
128
  return { ...miss, reason: `Compiler produced no valid rules from the instructions (${dropped.length} rejected).` };
127
129
  const checklist = { schema: SCHEMA_VERSION, fingerprint, model: config.compilerModel, compiledAt: Date.now(), rules, dropped };
128
- if (config.cacheChecklist)
130
+ // Only a fully validated checklist is reused; a partial one is retried on the next turn.
131
+ if (config.cacheChecklist && dropped.length === 0)
129
132
  store.putChecklist(checklist);
130
133
  return { ...miss, checklist };
131
134
  }
package/dist/judge.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { Checklist, Config, Evidence, Finding } from "./types.ts";
2
2
  import { type DecisionModel } from "./providers.ts";
3
+ /** Marks text the ledger cut. An event read back from it can no longer prove what it does not show. */
4
+ export declare const LEDGER_MARK = "\u2026[clipped in ledger]";
3
5
  export declare class EvidenceTooLarge extends Error {
4
6
  }
5
7
  /**
@@ -25,4 +27,6 @@ export declare function judge(input: {
25
27
  prior: Finding[];
26
28
  model: DecisionModel;
27
29
  config: Config;
30
+ stopReason?: () => string | null;
31
+ onUsage?: (tokens: number, costUSD: number) => void;
28
32
  }): Promise<JudgeResult>;
package/dist/judge.js CHANGED
@@ -8,11 +8,16 @@ const MIN_SATISFIED = 0.6;
8
8
  const CLIP_STEPS = [2000, 500, 120];
9
9
  const HISTORY_CLIP = 120;
10
10
  const HISTORY_SHARE = 0.3;
11
+ /** Marks text the ledger cut. An event read back from it can no longer prove what it does not show. */
12
+ export const LEDGER_MARK = "…[clipped in ledger]";
11
13
  export class EvidenceTooLarge extends Error {
12
14
  }
15
+ const ledgerClipped = (event) => [event.text, event.output, event.input].some((value) => typeof value === "string" && value.includes(LEDGER_MARK));
13
16
  function clip(text, limit) {
14
17
  return text.length <= limit ? text : `${text.slice(0, limit)}…[clipped ${text.length - limit} chars]`;
15
18
  }
19
+ /** Redact whole values before clipping: a cut secret no longer matches its pattern. */
20
+ const safeClip = (text, limit) => clip(redact(text), limit);
16
21
  function eventLine(event, limit) {
17
22
  const parts = [`#${event.seq} id=${event.id} ${event.kind}`];
18
23
  if (event.actor)
@@ -22,12 +27,12 @@ function eventLine(event, limit) {
22
27
  if (event.status)
23
28
  parts.push(`status=${event.status}`);
24
29
  if (event.input !== undefined)
25
- parts.push(`input=${clip(typeof event.input === "string" ? event.input : JSON.stringify(event.input), limit)}`);
30
+ parts.push(`input=${safeClip(typeof event.input === "string" ? event.input : JSON.stringify(event.input), limit)}`);
26
31
  if (event.output)
27
- parts.push(`output=${clip(event.output, limit)}`);
32
+ parts.push(`output=${safeClip(event.output, limit)}`);
28
33
  if (event.text)
29
- parts.push(`text=${clip(event.text, limit)}`);
30
- return redact(parts.join(" "));
34
+ parts.push(`text=${safeClip(event.text, limit)}`);
35
+ return parts.join(" ");
31
36
  }
32
37
  /**
33
38
  * The turn is clipped loudly and never loses an event: over budget means an incomplete audit. Older context may be
@@ -38,26 +43,27 @@ export function digest(evidence, prior, budgetChars, rawIDs = new Set(), preambl
38
43
  const historyLines = [];
39
44
  let used = 0;
40
45
  for (const event of [...history].reverse()) {
41
- const line = eventLine(event, HISTORY_CLIP);
46
+ const line = eventLine(event, rawIDs.has(event.id) ? budgetChars : HISTORY_CLIP);
42
47
  if (used + line.length > budgetChars * HISTORY_SHARE)
43
48
  break;
44
49
  used += line.length;
45
50
  historyLines.unshift(line);
46
51
  }
47
52
  const omitted = history.length - historyLines.length;
48
- const complete = evidence.complete && omitted === 0;
49
- const gaps = [...evidence.gaps, ...(omitted ? [`${omitted} older events omitted to fit the budget`] : [])];
53
+ const clippedInLedger = [...history, ...evidence.events].some(ledgerClipped);
54
+ const complete = evidence.complete && omitted === 0 && !clippedInLedger;
55
+ const gaps = [...evidence.gaps, ...(omitted ? [`${omitted} older events omitted to fit the budget`] : []), ...(clippedInLedger ? ["some retained events were clipped after the host compacted them"] : [])];
50
56
  const head = [
51
57
  preamble,
52
58
  `Evidence source: ${complete ? "known complete" : `INCOMPLETE (${gaps.join("; ") || "unknown gaps"})`}.`,
53
- `Current user request: ${redact(clip(evidence.userRequest, 4000))}`,
59
+ `Current user request: ${safeClip(evidence.userRequest, 4000)}`,
54
60
  prior.length ? `Unresolved findings from the previous audit of this turn: ${prior.map((finding) => `${finding.ruleID}: ${clip(finding.quote, 200)}`).join(" | ")}` : "",
55
61
  historyLines.length ? `Earlier turns, context only${omitted ? ` (${omitted} older events omitted)` : ""}:\n${historyLines.join("\n")}` : "",
56
62
  "Events of the turn under audit:",
57
63
  ].filter(Boolean);
58
64
  for (const limit of CLIP_STEPS) {
59
65
  const lines = evidence.events.map((event) => eventLine(event, rawIDs.has(event.id) ? budgetChars : limit));
60
- const text = [...head, ...lines, `Final reply: ${redact(clip(evidence.finalReply, 8000))}`].join("\n");
66
+ const text = [...head, ...lines, `Final reply: ${redact(evidence.finalReply)}`].join("\n");
61
67
  if (text.length <= budgetChars)
62
68
  return { text, clipped: limit !== CLIP_STEPS[0] || text.includes("…[clipped"), complete };
63
69
  }
@@ -78,11 +84,15 @@ export async function judge(input) {
78
84
  const ask = async (state, questions) => {
79
85
  const answers = {};
80
86
  for (let index = 0; index < questions.length; index += CHUNK) {
87
+ const stop = input.stopReason?.();
88
+ if (stop)
89
+ throw new Error(stop);
81
90
  const decisions = await withRetry(() => model.ask(state, questions.slice(index, index + CHUNK)), config);
82
91
  Object.assign(answers, decisions.answers);
83
92
  result.calls += 1;
84
93
  result.tokens += decisions.tokens ?? 0;
85
94
  result.costUSD += decisions.costUSD ?? 0;
95
+ input.onUsage?.(decisions.tokens ?? 0, decisions.costUSD ?? 0);
86
96
  }
87
97
  return answers;
88
98
  };
@@ -111,7 +121,10 @@ export async function judge(input) {
111
121
  { type: "predicate", name: "turn_waiting", instructions: QUESTIONS.waiting },
112
122
  ...open.flatMap((rule, i) => [predicate(`r${i}_applies`, rule, QUESTIONS.applies), predicate(`r${i}_deadline`, rule, QUESTIONS.deadline)]),
113
123
  ]);
114
- result.waiting = (scope.turn_waiting?.p ?? 0) >= 0.5;
124
+ const waiting = scope.turn_waiting?.p;
125
+ if (typeof waiting !== "number")
126
+ throw new Error("the model did not answer whether the agent is waiting for the user");
127
+ result.waiting = waiting >= 0.5;
115
128
  const applying = new Set(open.filter((_, i) => (scope[`r${i}_applies`]?.p ?? 1) >= 0.5).map((rule) => rule.id));
116
129
  const candidates = [];
117
130
  open.forEach((rule, i) => {
@@ -121,7 +134,7 @@ export async function judge(input) {
121
134
  return void done.set(rule.id, finding(rule, "UNKNOWN", "The decision model gave no answer.", { uncertainty: scope[`r${i}_applies`]?.error ?? scope[`r${i}_deadline`]?.error ?? "missing answer" }));
122
135
  if (applies < 0.5)
123
136
  return void done.set(rule.id, finding(rule, "NOT_APPLICABLE", "Does not govern this turn.", { probability: applies }));
124
- if (rule.supersededBy && applying.has(rule.supersededBy))
137
+ if (rule.supersedable && rule.supersededBy && applying.has(rule.supersededBy))
125
138
  return void done.set(rule.id, finding(rule, "NOT_APPLICABLE", `Superseded by applicable rule ${rule.supersededBy}.`));
126
139
  if (deadline < 0.5)
127
140
  return void done.set(rule.id, finding(rule, "PENDING", "Its deadline has not occurred at this boundary.", { probability: deadline }));
@@ -140,23 +153,29 @@ export async function judge(input) {
140
153
  instructions: `${ruleText(rule)}\nQuestion: ${text}`,
141
154
  choices: events.map((event) => ({ value: event.id, description: clip(eventLine(event, 160), 240) })),
142
155
  });
143
- // Ambiguous verdicts get one recheck with the disputed spans unclipped.
144
- const ambiguous = candidates.filter(({ i }) => {
156
+ // A verdict resting on clipped evidence gets one recheck with the disputed span in full:
157
+ // ambiguous ones, and any breach that rests on absence, since the proof may sit in the clipped part.
158
+ const disputed = candidates.filter(({ i }) => {
145
159
  const p = verdict[`r${i}_violated`]?.p ?? 0;
146
- return p >= 0.5 && p < config.minViolationConfidence;
160
+ return p >= 0.5 && (p < config.minViolationConfidence || (verdict[`r${i}_absence`]?.p ?? 1) >= 0.5);
147
161
  });
148
- if (first.clipped && ambiguous.length && inTurn.length) {
149
- const spans = await ask(first.text, ambiguous.map(({ rule, i }) => cite(`r${i}_span`, rule, "Which event most directly bears on whether this rule was breached?")));
150
- const raw = new Set(ambiguous.flatMap(({ i }) => spans[`r${i}_span`]?.choice ?? []));
162
+ const rechecked = new Set();
163
+ if (first.clipped && disputed.length && inTurn.length) {
164
+ const spans = await ask(first.text, disputed.map(({ rule, i }) => cite(`r${i}_span`, rule, "Which event's full content most directly bears on whether this rule was breached?", everywhere)));
165
+ const restored = disputed.filter(({ i }) => knownAnywhere.has(spans[`r${i}_span`]?.choice ?? ""));
166
+ const raw = new Set(restored.map(({ i }) => spans[`r${i}_span`].choice));
151
167
  try {
152
- const again = await ask(digest(evidence, prior, budget * 2, raw, preamble).text, ambiguous.map(({ rule, i }) => predicate(`r${i}_violated`, rule, QUESTIONS.violated)));
168
+ const again = await ask(digest(evidence, prior, budget * 2, raw, preamble).text, restored.map(({ rule, i }) => predicate(`r${i}_violated`, rule, QUESTIONS.violated)));
153
169
  Object.assign(verdict, again);
170
+ for (const { i } of restored)
171
+ rechecked.add(i);
154
172
  }
155
173
  catch (error) {
156
174
  if (!(error instanceof EvidenceTooLarge))
157
175
  throw error;
158
176
  }
159
177
  }
178
+ const reported = new Set(prior.map((finding) => finding.ruleID));
160
179
  const breached = [];
161
180
  const complied = [];
162
181
  for (const candidate of candidates) {
@@ -165,9 +184,11 @@ export async function judge(input) {
165
184
  const satisfied = verdict[`r${i}_satisfied`]?.p ?? 0;
166
185
  if (violated === undefined)
167
186
  done.set(rule.id, finding(rule, "UNKNOWN", "The decision model gave no answer.", { uncertainty: verdict[`r${i}_violated`]?.error ?? "missing answer" }));
168
- else if (violated >= config.minViolationConfidence) {
169
- if ((verdict[`r${i}_absence`]?.p ?? 1) >= 0.5 && !first.complete)
170
- done.set(rule.id, finding(rule, "UNKNOWN", "The breach rests only on missing events and the evidence source is incomplete.", { probability: violated, uncertainty: "incomplete evidence" }));
187
+ // A breach an earlier audit of this turn already proved stays on the repair path even if this verdict is less sure.
188
+ else if (violated >= config.minViolationConfidence || (violated >= 0.5 && reported.has(rule.id))) {
189
+ const unprovable = !first.complete || (first.clipped && !rechecked.has(i));
190
+ if ((verdict[`r${i}_absence`]?.p ?? 1) >= 0.5 && unprovable)
191
+ done.set(rule.id, finding(rule, "UNKNOWN", "The breach rests only on missing events and the evidence is incomplete.", { probability: violated, uncertainty: "incomplete evidence" }));
171
192
  else
172
193
  breached.push(candidate);
173
194
  }
@@ -179,7 +200,6 @@ export async function judge(input) {
179
200
  done.set(rule.id, finding(rule, "UNKNOWN", "Neither compliance nor a breach is supported by the evidence.", { probability: satisfied, uncertainty: "insufficient evidence" }));
180
201
  }
181
202
  // Stage 3: evidence and repair routing, asked only for what needs them.
182
- const reported = new Set(prior.map((finding) => finding.ruleID));
183
203
  const detail = everywhere.length
184
204
  ? await ask(first.text, [
185
205
  ...breached.flatMap(({ rule, i }) => [
package/dist/redact.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const PATTERNS = [
2
- [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED PRIVATE KEY]"],
2
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, "[REDACTED PRIVATE KEY]"],
3
3
  [/\b(sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|npm_[A-Za-z0-9]{20,}|xox[abprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{30,})\b/g, "[REDACTED TOKEN]"],
4
4
  [/\b(authorization\s*[:=]\s*(?:bearer\s+)?)[^\s"']+/gi, "$1[REDACTED]"],
5
5
  [/\b(bearer\s+)[A-Za-z0-9._~+/-]{16,}=*/gi, "$1[REDACTED]"],
package/dist/store.d.ts CHANGED
@@ -20,5 +20,8 @@ export declare class Store {
20
20
  /** Evidence that must survive host compaction. Holds redacted, clipped events by default. */
21
21
  readLedger(sessionID: string): ExecEvent[];
22
22
  writeLedger(sessionID: string, events: ExecEvent[]): void;
23
+ /** Lazily loaded instruction files, which the host may not show again after compaction. */
24
+ readNested(sessionID: string): Record<string, string>;
25
+ writeNested(sessionID: string, nested: Record<string, string>): void;
23
26
  cleanup(retentionDays: number, now?: number): void;
24
27
  }
package/dist/store.js CHANGED
@@ -142,6 +142,13 @@ export class Store {
142
142
  writeLedger(sessionID, events) {
143
143
  writeAtomic(join(this.sessionDir(sessionID), "ledger.json"), JSON.stringify(events));
144
144
  }
145
+ /** Lazily loaded instruction files, which the host may not show again after compaction. */
146
+ readNested(sessionID) {
147
+ return readJson(join(this.sessionDir(sessionID), "nested.json")) ?? {};
148
+ }
149
+ writeNested(sessionID, nested) {
150
+ writeAtomic(join(this.sessionDir(sessionID), "nested.json"), JSON.stringify(nested));
151
+ }
145
152
  cleanup(retentionDays, now = Date.now()) {
146
153
  const cutoff = now - retentionDays * 86_400_000;
147
154
  const prune = (dir) => {
package/package.json CHANGED
@@ -1,12 +1,28 @@
1
1
  {
2
2
  "name": "fidelis",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Harness-agnostic AI agent auditor core: checklist compiler, judge pipeline and session state machine.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
- "repository": { "type": "git", "url": "git+https://github.com/insign/fidelis.git", "directory": "packages/core" },
8
- "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
9
- "files": ["dist", "README.md"],
10
- "publishConfig": { "access": "public" },
11
- "engines": { "node": ">=22.18" }
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/insign/fidelis.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "engines": {
26
+ "node": ">=22.18"
27
+ }
12
28
  }