@gmickel/gno 1.43.0 → 1.45.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/assets/skill/SKILL.md +3 -0
- package/assets/skill/recipes/memory-file-decision.md +76 -0
- package/assets/skill/recipes/memory-scoped-recall.md +66 -0
- package/assets/skill/recipes/memory-supersede-fact.md +68 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.43.0.zip → gno-browser-clipper-v1.45.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.45.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +89 -8
- package/spec/mcp.md +12 -0
- package/spec/output-schemas/changes-follow-event.schema.json +35 -0
- package/spec/output-schemas/index-receipt.schema.json +135 -0
- package/spec/output-schemas/process-status.schema.json +76 -0
- package/src/cli/commands/agents/block.ts +9 -8
- package/src/cli/commands/changes-follow.ts +167 -0
- package/src/cli/commands/changes.ts +63 -0
- package/src/cli/commands/daemon.ts +35 -0
- package/src/cli/commands/doctor.ts +71 -0
- package/src/cli/commands/embed.ts +236 -178
- package/src/cli/commands/index-cmd.ts +238 -57
- package/src/cli/program.ts +94 -4
- package/src/config/types.ts +48 -0
- package/src/core/capture-sync.ts +144 -0
- package/src/core/capture.ts +10 -0
- package/src/core/findings-records.ts +381 -0
- package/src/core/findings-run-state.ts +282 -0
- package/src/embed/stage-state.ts +199 -0
- package/src/mcp/tools/capture.ts +91 -136
- package/src/serve/capture-service.ts +227 -53
- package/src/serve/findings-pass.ts +335 -0
- package/src/serve/resident-runtime.ts +42 -0
- package/src/serve/routes/api.ts +14 -14
- package/browser-extension/artifacts/gno-browser-clipper-v1.43.0.zip.sha256 +0 -1
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon-only scheduled findings pass: read-only audit -> findings records.
|
|
3
|
+
*
|
|
4
|
+
* Report-only by construction: the audit never repairs, the writer only
|
|
5
|
+
* touches records it owns under the findings collection root, and every
|
|
6
|
+
* attempt persists its outcome so a starved or failing scheduler is visible
|
|
7
|
+
* through `gno daemon --status` and `gno doctor` without debug logs.
|
|
8
|
+
*
|
|
9
|
+
* The audit runs without the shared write lease; only the record write takes
|
|
10
|
+
* it, so a long audit never blocks capture or CLI writers.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Config } from "../config/types";
|
|
14
|
+
import type { AuditRunResult } from "../core/audit";
|
|
15
|
+
import type {
|
|
16
|
+
FindingsRunCounts,
|
|
17
|
+
FindingsRunOutcome,
|
|
18
|
+
FindingsRunStateRecord,
|
|
19
|
+
FindingsSchedule,
|
|
20
|
+
} from "../core/findings-run-state";
|
|
21
|
+
import type { SqliteAdapter } from "../store/sqlite/adapter";
|
|
22
|
+
|
|
23
|
+
import { AUDIT_CATEGORIES } from "../core/audit";
|
|
24
|
+
import { runWorkspaceAudit } from "../core/audit-workspace";
|
|
25
|
+
import { applyFindingsRecords } from "../core/findings-records";
|
|
26
|
+
import {
|
|
27
|
+
createPendingFindingsRunState,
|
|
28
|
+
EMPTY_FINDINGS_COUNTS,
|
|
29
|
+
writeFindingsRunState,
|
|
30
|
+
} from "../core/findings-run-state";
|
|
31
|
+
import { acquireCliWriteLease } from "../core/write-lease";
|
|
32
|
+
|
|
33
|
+
/** Audit report cap; matches the audit schema ceiling. */
|
|
34
|
+
const FINDINGS_AUDIT_MAX_FINDINGS = 1000;
|
|
35
|
+
const LEASE_HOLDER_COMMAND = "gno daemon (findings pass)";
|
|
36
|
+
const CONTROL_CHARS = /\p{Cc}/gu;
|
|
37
|
+
|
|
38
|
+
/** Error text lands in a JSON state file and status lines: keep it printable and bounded. */
|
|
39
|
+
const boundErrorText = (text: string): string =>
|
|
40
|
+
text.replace(CONTROL_CHARS, "").slice(0, 512);
|
|
41
|
+
|
|
42
|
+
export interface FindingsPassResult {
|
|
43
|
+
outcome: FindingsRunOutcome;
|
|
44
|
+
counts: FindingsRunCounts;
|
|
45
|
+
durationMs: number;
|
|
46
|
+
error: string | null;
|
|
47
|
+
/** The state record persisted for this attempt. */
|
|
48
|
+
record: FindingsRunStateRecord;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type FindingsPassAttempt = Omit<FindingsPassResult, "record">;
|
|
52
|
+
|
|
53
|
+
export interface FindingsPassDeps {
|
|
54
|
+
store: SqliteAdapter;
|
|
55
|
+
getConfig: () => Config;
|
|
56
|
+
schedule: FindingsSchedule;
|
|
57
|
+
dbPath: string;
|
|
58
|
+
indexName: string;
|
|
59
|
+
statePath: string;
|
|
60
|
+
now?: () => Date;
|
|
61
|
+
/** Overridable for tests; defaults to the shared `.mcp-write.lock` lease. */
|
|
62
|
+
acquireLease?: (dbPath: string) => Promise<{
|
|
63
|
+
ok: boolean;
|
|
64
|
+
release?: () => Promise<void>;
|
|
65
|
+
holder?: string | null;
|
|
66
|
+
}>;
|
|
67
|
+
runAudit?: typeof runWorkspaceAudit;
|
|
68
|
+
/** Overridable for tests; defaults to the atomic state-file writer. */
|
|
69
|
+
writeState?: typeof writeFindingsRunState;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const defaultAcquireLease: NonNullable<
|
|
73
|
+
FindingsPassDeps["acquireLease"]
|
|
74
|
+
> = async (dbPath) => {
|
|
75
|
+
const lease = await acquireCliWriteLease({
|
|
76
|
+
dbPath,
|
|
77
|
+
waitMs: 0,
|
|
78
|
+
noWait: true,
|
|
79
|
+
command: LEASE_HOLDER_COMMAND,
|
|
80
|
+
});
|
|
81
|
+
return lease.ok
|
|
82
|
+
? { ok: true, release: lease.release }
|
|
83
|
+
: { ok: false, holder: lease.holder };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const settledRuleIds = (result: AuditRunResult): Set<string> => {
|
|
87
|
+
if (!result.ok) return new Set();
|
|
88
|
+
return new Set(
|
|
89
|
+
result.report.rules
|
|
90
|
+
.filter((rule) => rule.status === "pass" || rule.status === "fail")
|
|
91
|
+
.map((rule) => rule.ruleId)
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* One pass. Never throws: every failure lands in the persisted state, and a
|
|
97
|
+
* failure to persist lands as `failed` in the returned record instead.
|
|
98
|
+
*/
|
|
99
|
+
export async function runFindingsPass(
|
|
100
|
+
deps: FindingsPassDeps,
|
|
101
|
+
previous: FindingsRunStateRecord,
|
|
102
|
+
signal?: AbortSignal
|
|
103
|
+
): Promise<FindingsPassResult> {
|
|
104
|
+
const now = deps.now ?? (() => new Date());
|
|
105
|
+
const startedAt = now();
|
|
106
|
+
const persist = async (
|
|
107
|
+
result: FindingsPassAttempt
|
|
108
|
+
): Promise<FindingsPassResult> => {
|
|
109
|
+
const finishedAt = now();
|
|
110
|
+
const record: FindingsRunStateRecord = {
|
|
111
|
+
...previous,
|
|
112
|
+
collection: deps.schedule.collection.name,
|
|
113
|
+
cadence: deps.schedule.cadence,
|
|
114
|
+
lastOutcome: result.outcome,
|
|
115
|
+
lastRunAt: startedAt.toISOString(),
|
|
116
|
+
lastSuccessAt:
|
|
117
|
+
result.outcome === "success"
|
|
118
|
+
? finishedAt.toISOString()
|
|
119
|
+
: previous.lastSuccessAt,
|
|
120
|
+
nextDueAt: new Date(
|
|
121
|
+
finishedAt.getTime() + deps.schedule.cadenceMs
|
|
122
|
+
).toISOString(),
|
|
123
|
+
durationMs: result.durationMs,
|
|
124
|
+
counts: result.outcome === "success" ? result.counts : previous.counts,
|
|
125
|
+
error: result.error,
|
|
126
|
+
};
|
|
127
|
+
try {
|
|
128
|
+
await (deps.writeState ?? writeFindingsRunState)(deps.statePath, record);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// An unwritable state file must not kill the daemon loop, but it must
|
|
131
|
+
// not vanish either: the in-memory record (next pass's `previous`, the
|
|
132
|
+
// daemon log via onResult) carries the failure until a write lands.
|
|
133
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
134
|
+
const failed: FindingsRunStateRecord = {
|
|
135
|
+
...record,
|
|
136
|
+
lastOutcome: "failed",
|
|
137
|
+
error: boundErrorText(
|
|
138
|
+
`state write failed: ${message}${record.error ? ` (after: ${record.error})` : ""}`
|
|
139
|
+
),
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
outcome: "failed",
|
|
143
|
+
counts: failed.counts ?? result.counts,
|
|
144
|
+
durationMs: result.durationMs,
|
|
145
|
+
error: failed.error,
|
|
146
|
+
record: failed,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { ...result, record };
|
|
150
|
+
};
|
|
151
|
+
const fail = (error: string): Promise<FindingsPassResult> =>
|
|
152
|
+
persist({
|
|
153
|
+
outcome: "failed",
|
|
154
|
+
counts: previous.counts ?? EMPTY_FINDINGS_COUNTS,
|
|
155
|
+
durationMs: now().getTime() - startedAt.getTime(),
|
|
156
|
+
error: boundErrorText(error),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// The audit is read-only: run it without the write lease so MCP/REST
|
|
160
|
+
// capture and CLI writers are never blocked behind a long audit. Only the
|
|
161
|
+
// findings-record write below needs the lease.
|
|
162
|
+
let audit: AuditRunResult;
|
|
163
|
+
let allowResolve: boolean;
|
|
164
|
+
try {
|
|
165
|
+
const config = deps.getConfig();
|
|
166
|
+
const findingsCollection = deps.schedule.collection.name;
|
|
167
|
+
const audited = config.collections
|
|
168
|
+
.map((collection) => collection.name)
|
|
169
|
+
.filter((name) => name !== findingsCollection);
|
|
170
|
+
if (audited.length === 0) {
|
|
171
|
+
return persist({
|
|
172
|
+
outcome: "success",
|
|
173
|
+
counts: { ...EMPTY_FINDINGS_COUNTS },
|
|
174
|
+
durationMs: now().getTime() - startedAt.getTime(),
|
|
175
|
+
error: null,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
audit = await (deps.runAudit ?? runWorkspaceAudit)({
|
|
179
|
+
store: deps.store,
|
|
180
|
+
config,
|
|
181
|
+
collections: config.collections,
|
|
182
|
+
indexName: deps.indexName,
|
|
183
|
+
categories: [...AUDIT_CATEGORIES],
|
|
184
|
+
collectionFilters: audited,
|
|
185
|
+
maxFindings: FINDINGS_AUDIT_MAX_FINDINGS,
|
|
186
|
+
signal,
|
|
187
|
+
now: startedAt,
|
|
188
|
+
});
|
|
189
|
+
if (!audit.ok) return fail(`audit failed: ${audit.error}`);
|
|
190
|
+
if (audit.report.status === "failed") {
|
|
191
|
+
return fail("audit reported status failed");
|
|
192
|
+
}
|
|
193
|
+
allowResolve =
|
|
194
|
+
audit.report.status === "complete" &&
|
|
195
|
+
!audit.report.counts.findings.truncated;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Write phase: take the shared lease only for as long as the records are
|
|
201
|
+
// being applied. A busy lease at this point still lands as skipped_lease.
|
|
202
|
+
const lease = await (deps.acquireLease ?? defaultAcquireLease)(deps.dbPath);
|
|
203
|
+
if (!lease.ok) {
|
|
204
|
+
return persist({
|
|
205
|
+
outcome: "skipped_lease",
|
|
206
|
+
counts: previous.counts ?? EMPTY_FINDINGS_COUNTS,
|
|
207
|
+
durationMs: now().getTime() - startedAt.getTime(),
|
|
208
|
+
error: lease.holder
|
|
209
|
+
? boundErrorText(`lease held by ${lease.holder}`)
|
|
210
|
+
: "lease held",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const applied = await applyFindingsRecords({
|
|
215
|
+
root: deps.schedule.collection.path,
|
|
216
|
+
findings: audit.report.findings,
|
|
217
|
+
settledRuleIds: settledRuleIds(audit),
|
|
218
|
+
allowResolve,
|
|
219
|
+
now: startedAt,
|
|
220
|
+
});
|
|
221
|
+
return persist({
|
|
222
|
+
outcome: "success",
|
|
223
|
+
counts: {
|
|
224
|
+
findings: audit.report.counts.findings.total,
|
|
225
|
+
written: applied.written,
|
|
226
|
+
reopened: applied.reopened,
|
|
227
|
+
resolved: applied.resolved,
|
|
228
|
+
deleted: applied.deleted,
|
|
229
|
+
open: applied.open,
|
|
230
|
+
},
|
|
231
|
+
durationMs: now().getTime() - startedAt.getTime(),
|
|
232
|
+
error: null,
|
|
233
|
+
});
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
236
|
+
} finally {
|
|
237
|
+
await lease.release?.().catch(() => undefined);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface FindingsSchedulerOptions {
|
|
242
|
+
deps: FindingsPassDeps;
|
|
243
|
+
startBackgroundWork: (
|
|
244
|
+
operation: (signal: AbortSignal) => Promise<void>
|
|
245
|
+
) => boolean;
|
|
246
|
+
/** Called after every attempt; the daemon logs failures only. */
|
|
247
|
+
onResult?: (result: FindingsPassResult) => void;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Fixed-cadence timer over the daemon's background-work tracker. One pass at
|
|
252
|
+
* a time; a pass still running when the next tick fires is simply skipped
|
|
253
|
+
* (the next tick is armed after the pass completes), so cadence is a floor.
|
|
254
|
+
*/
|
|
255
|
+
export class FindingsScheduler {
|
|
256
|
+
readonly #options: FindingsSchedulerOptions;
|
|
257
|
+
#state: FindingsRunStateRecord;
|
|
258
|
+
#timer: ReturnType<typeof setTimeout> | null = null;
|
|
259
|
+
#running: Promise<FindingsPassResult> | null = null;
|
|
260
|
+
#disposed = false;
|
|
261
|
+
|
|
262
|
+
constructor(options: FindingsSchedulerOptions) {
|
|
263
|
+
this.#options = options;
|
|
264
|
+
this.#state = createPendingFindingsRunState(
|
|
265
|
+
options.deps.schedule,
|
|
266
|
+
(options.deps.now ?? (() => new Date()))()
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
get state(): FindingsRunStateRecord {
|
|
271
|
+
return this.#state;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Persist the pending state and arm the first tick. */
|
|
275
|
+
async start(): Promise<void> {
|
|
276
|
+
if (this.#disposed) return;
|
|
277
|
+
const { deps } = this.#options;
|
|
278
|
+
try {
|
|
279
|
+
await (deps.writeState ?? writeFindingsRunState)(
|
|
280
|
+
deps.statePath,
|
|
281
|
+
this.#state
|
|
282
|
+
);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
// An unwritable state file must not stop the loop: keep the pending
|
|
285
|
+
// record in memory with the write error attached and still arm the
|
|
286
|
+
// first tick; the next successful pass write carries a fresh record.
|
|
287
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
288
|
+
this.#state = {
|
|
289
|
+
...this.#state,
|
|
290
|
+
error: `state write failed: ${message}`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
this.#arm();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Run a pass now (tests, live verification). Coalesces with an in-flight pass. */
|
|
297
|
+
triggerNow(signal?: AbortSignal): Promise<FindingsPassResult> {
|
|
298
|
+
if (this.#running) return this.#running;
|
|
299
|
+
const run = runFindingsPass(this.#options.deps, this.#state, signal)
|
|
300
|
+
.then((result) => {
|
|
301
|
+
this.#state = result.record;
|
|
302
|
+
this.#options.onResult?.(result);
|
|
303
|
+
return result;
|
|
304
|
+
})
|
|
305
|
+
.finally(() => {
|
|
306
|
+
this.#running = null;
|
|
307
|
+
this.#arm();
|
|
308
|
+
});
|
|
309
|
+
this.#running = run;
|
|
310
|
+
return run;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
dispose(): void {
|
|
314
|
+
this.#disposed = true;
|
|
315
|
+
if (this.#timer) clearTimeout(this.#timer);
|
|
316
|
+
this.#timer = null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#arm(): void {
|
|
320
|
+
if (this.#disposed) return;
|
|
321
|
+
if (this.#timer) clearTimeout(this.#timer);
|
|
322
|
+
this.#timer = setTimeout(
|
|
323
|
+
() => {
|
|
324
|
+
this.#timer = null;
|
|
325
|
+
if (this.#disposed || this.#running) return;
|
|
326
|
+
const started = this.#options.startBackgroundWork(async (signal) => {
|
|
327
|
+
await this.triggerNow(signal);
|
|
328
|
+
});
|
|
329
|
+
if (!started) this.#arm();
|
|
330
|
+
},
|
|
331
|
+
this.#options.deps.schedule.cadenceMs
|
|
332
|
+
);
|
|
333
|
+
this.#timer.unref?.();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -35,6 +35,11 @@ import { SavedCapsuleReverificationScheduler } from "../core/capsule-reverificat
|
|
|
35
35
|
import { collectionEgressPolicyEpoch } from "../core/collection-egress-policy-service";
|
|
36
36
|
import { authorizeCurrentEgress } from "../core/egress-authorization";
|
|
37
37
|
import { acquireWriteLock } from "../core/file-lock";
|
|
38
|
+
import {
|
|
39
|
+
deleteFindingsRunState,
|
|
40
|
+
findingsRunStatePath,
|
|
41
|
+
resolveFindingsSchedule,
|
|
42
|
+
} from "../core/findings-run-state";
|
|
38
43
|
import { JobManager } from "../core/job-manager";
|
|
39
44
|
import { recordContentMutation } from "../core/mutation-generations";
|
|
40
45
|
import { defaultSyncService, withContentTypeRules } from "../ingestion";
|
|
@@ -50,6 +55,7 @@ import {
|
|
|
50
55
|
type ServerContext,
|
|
51
56
|
} from "./context";
|
|
52
57
|
import { createEmbedScheduler } from "./embed-scheduler";
|
|
58
|
+
import { FindingsScheduler, type FindingsPassResult } from "./findings-pass";
|
|
53
59
|
import { AdmissionController, ReaderGate } from "./resident-admission";
|
|
54
60
|
import { ResidentBackgroundWork } from "./resident-background-work";
|
|
55
61
|
import { buildResidentStatusSnapshot } from "./resident-status";
|
|
@@ -68,6 +74,8 @@ export interface ResidentRuntimeOptions {
|
|
|
68
74
|
offline?: boolean;
|
|
69
75
|
eventBus?: DocumentEventBus | null;
|
|
70
76
|
watchCallbacks?: CollectionWatchCallbacks;
|
|
77
|
+
/** Daemon mode only: observes every scheduled findings-pass attempt. */
|
|
78
|
+
onFindingsResult?: (result: FindingsPassResult) => void;
|
|
71
79
|
readerLimit?: number;
|
|
72
80
|
readerQueueLimit?: number;
|
|
73
81
|
shutdownDeadlineMs?: number;
|
|
@@ -100,6 +108,8 @@ export interface ResidentRuntime {
|
|
|
100
108
|
readonly readerGate: ReaderGate;
|
|
101
109
|
readonly jobManager: JobManager;
|
|
102
110
|
readonly capsuleReverificationScheduler: SavedCapsuleReverificationScheduler;
|
|
111
|
+
/** Present only when daemon mode runs with `findings.enabled`. */
|
|
112
|
+
readonly findingsScheduler: FindingsScheduler | null;
|
|
103
113
|
readonly modelManager: ModelManager;
|
|
104
114
|
readonly mcpContext: ToolContext;
|
|
105
115
|
readonly generations: ResidentGeneration;
|
|
@@ -194,6 +204,15 @@ export async function startResidentRuntime(
|
|
|
194
204
|
};
|
|
195
205
|
}
|
|
196
206
|
|
|
207
|
+
const mode: ResidentMode = options.mode ?? "serve";
|
|
208
|
+
const findingsResolution =
|
|
209
|
+
mode === "daemon"
|
|
210
|
+
? resolveFindingsSchedule(initialConfig)
|
|
211
|
+
: ({ ok: true, enabled: false } as const);
|
|
212
|
+
if (!findingsResolution.ok) {
|
|
213
|
+
return { success: false, error: findingsResolution.error };
|
|
214
|
+
}
|
|
215
|
+
|
|
197
216
|
await (deps.ensureDirectories ?? ensureDirectories)();
|
|
198
217
|
const dbPath = getIndexDbPath(options.index);
|
|
199
218
|
const ownerLockPath = join(dirname(dbPath), ".resident-owner.lock");
|
|
@@ -334,6 +353,27 @@ export async function startResidentRuntime(
|
|
|
334
353
|
const backgroundWork = new ResidentBackgroundWork(
|
|
335
354
|
() => !disposed && admission.accepting
|
|
336
355
|
);
|
|
356
|
+
const findingsStatePath = findingsRunStatePath(dbPath);
|
|
357
|
+
let findingsScheduler: FindingsScheduler | null = null;
|
|
358
|
+
if (findingsResolution.enabled) {
|
|
359
|
+
findingsScheduler = new FindingsScheduler({
|
|
360
|
+
deps: {
|
|
361
|
+
store,
|
|
362
|
+
getConfig: () => ctxHolder.config,
|
|
363
|
+
schedule: findingsResolution.schedule,
|
|
364
|
+
dbPath,
|
|
365
|
+
indexName: canonicalizeIndexName(options.index ?? DEFAULT_INDEX_NAME),
|
|
366
|
+
statePath: findingsStatePath,
|
|
367
|
+
},
|
|
368
|
+
startBackgroundWork: (operation) => backgroundWork.start(operation),
|
|
369
|
+
onResult: options.onFindingsResult,
|
|
370
|
+
});
|
|
371
|
+
await findingsScheduler.start();
|
|
372
|
+
} else if (mode === "daemon") {
|
|
373
|
+
// Findings are off: a state file from an earlier configuration would
|
|
374
|
+
// otherwise keep reporting a schedule that no longer exists.
|
|
375
|
+
await deleteFindingsRunState(findingsStatePath);
|
|
376
|
+
}
|
|
337
377
|
capsuleReverificationScheduler = new SavedCapsuleReverificationScheduler({
|
|
338
378
|
deps: {
|
|
339
379
|
store,
|
|
@@ -414,6 +454,7 @@ export async function startResidentRuntime(
|
|
|
414
454
|
readerGate,
|
|
415
455
|
jobManager,
|
|
416
456
|
capsuleReverificationScheduler,
|
|
457
|
+
findingsScheduler,
|
|
417
458
|
modelManager,
|
|
418
459
|
mcpContext,
|
|
419
460
|
generations,
|
|
@@ -540,6 +581,7 @@ export async function startResidentRuntime(
|
|
|
540
581
|
options.shutdownAbortSettleMs ?? DEFAULT_SHUTDOWN_DEADLINE_MS
|
|
541
582
|
);
|
|
542
583
|
if (deadlineReached) shutdownState = "deadline";
|
|
584
|
+
findingsScheduler?.dispose();
|
|
543
585
|
await backgroundWork.cancelAndDrain();
|
|
544
586
|
await capsuleReverificationScheduler.dispose();
|
|
545
587
|
await jobManager.shutdown().catch(() => undefined);
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -157,8 +157,10 @@ import {
|
|
|
157
157
|
import { exportPublishArtifact } from "../../publish/export-service";
|
|
158
158
|
import { buildBrowseTree, normalizeBrowsePath } from "../browse-tree";
|
|
159
159
|
import {
|
|
160
|
+
classifyResidentCaptureError,
|
|
160
161
|
executeResidentCapturePlan,
|
|
161
162
|
planResidentCapture,
|
|
163
|
+
type ResidentCaptureDependencies,
|
|
162
164
|
} from "../capture-service";
|
|
163
165
|
import { parseClosedJson } from "../closed-json";
|
|
164
166
|
import { applyConfigChange, applyConfigChangeTyped } from "../config-sync";
|
|
@@ -3629,9 +3631,7 @@ export async function handleCreateCapture(
|
|
|
3629
3631
|
ctxHolder: ContextHolder,
|
|
3630
3632
|
store: SqliteAdapter,
|
|
3631
3633
|
req: Request,
|
|
3632
|
-
deps
|
|
3633
|
-
syncCollection?: typeof defaultSyncService.syncCollection;
|
|
3634
|
-
}
|
|
3634
|
+
deps: Omit<ResidentCaptureDependencies, "mode" | "syncCollection"> = {}
|
|
3635
3635
|
): Promise<Response> {
|
|
3636
3636
|
let body: CreateCaptureRequestBody;
|
|
3637
3637
|
try {
|
|
@@ -3669,21 +3669,21 @@ export async function handleCreateCapture(
|
|
|
3669
3669
|
if (!planned.ok) {
|
|
3670
3670
|
return errorResponse(planned.code, planned.message, planned.status);
|
|
3671
3671
|
}
|
|
3672
|
+
// Write + lexical sync complete under the shared write lease before the
|
|
3673
|
+
// response: 201 only once the capture is retrievable (fn-132 R1).
|
|
3672
3674
|
try {
|
|
3673
|
-
const result = await executeResidentCapturePlan(
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
deps
|
|
3678
|
-
);
|
|
3675
|
+
const result = await executeResidentCapturePlan(ctxHolder, store, planned, {
|
|
3676
|
+
...deps,
|
|
3677
|
+
mode: "await-sync",
|
|
3678
|
+
});
|
|
3679
3679
|
return jsonResponse(result.body, result.status);
|
|
3680
3680
|
} catch (error) {
|
|
3681
|
+
const shape = classifyResidentCaptureError(error, planned);
|
|
3681
3682
|
return errorResponse(
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
500
|
|
3683
|
+
shape.code,
|
|
3684
|
+
shape.message,
|
|
3685
|
+
shape.status,
|
|
3686
|
+
shape.details
|
|
3687
3687
|
);
|
|
3688
3688
|
}
|
|
3689
3689
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
c0ebec1aed09df105e0b8bacec38818b39b4173c25a0bdcaa6bd3a4774a2e548 gno-browser-clipper-v1.43.0.zip
|