@broberg/lens-daemon 0.1.29 → 0.1.31
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/package.json +3 -3
- package/src/artifact-uploads.ts +34 -0
- package/src/flow.ts +2 -1
- package/src/service.ts +15 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@broberg/lens-daemon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "The Lens module of the cardmem daemon (flows, critic, coverage, capture sessions, the /lens/* routes and the cardmem-lens MCP) as a package. The daemon mounts createLensRouter(deps) + createLensMcpRouter(deps). Bun runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"sharp": "^0.33.5",
|
|
16
16
|
"turndown": "^7.2.4",
|
|
17
17
|
"zod": "^3.23.8",
|
|
18
|
-
"@broberg/lens-engine": "0.14.
|
|
19
|
-
"@broberg/lens-svg": "0.1.
|
|
18
|
+
"@broberg/lens-engine": "0.14.1",
|
|
19
|
+
"@broberg/lens-svg": "0.1.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@happy-dom/global-registrator": "^20.10.6",
|
package/src/artifact-uploads.ts
CHANGED
|
@@ -121,6 +121,40 @@ export function enqueueArtifact(db: Database, projectSlug: string, key: string |
|
|
|
121
121
|
).run(key, projectSlug, skipReason ? 'skipped' : 'pending', Date.now(), skipReason ?? null);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/** lens F006.1 — where one stored artifact stands on its way to the cloud, as a run
|
|
125
|
+
* report shows it. A still with no queue row is said to be NOT QUEUED — never
|
|
126
|
+
* rounded to "pending", which would promise an upload nothing is going to make. */
|
|
127
|
+
export type CloudCopy =
|
|
128
|
+
| { status: 'done'; url: string }
|
|
129
|
+
| { status: 'pending' }
|
|
130
|
+
/** Failed at least once and will be tried again (the row is back in the queue). */
|
|
131
|
+
| { status: 'retrying'; error: string; attempts: number }
|
|
132
|
+
/** Given up after MAX_ATTEMPTS, or the bytes are gone. */
|
|
133
|
+
| { status: 'failed'; error: string; attempts: number }
|
|
134
|
+
| { status: 'skipped'; reason: string }
|
|
135
|
+
| { status: 'not_queued' };
|
|
136
|
+
|
|
137
|
+
export function cloudCopiesOf(db: Database, keys: string[]): Map<string, CloudCopy> {
|
|
138
|
+
const out = new Map<string, CloudCopy>();
|
|
139
|
+
if (keys.length === 0) return out;
|
|
140
|
+
const rows = db
|
|
141
|
+
.query(`SELECT * FROM lens_artifact_uploads WHERE artifact_key IN (${keys.map(() => '?').join(',')})`)
|
|
142
|
+
.all(...keys) as UploadRow[];
|
|
143
|
+
const byKey = new Map(rows.map((r) => [r.artifact_key, r]));
|
|
144
|
+
for (const k of keys) {
|
|
145
|
+
const r = byKey.get(k);
|
|
146
|
+
if (!r) out.set(k, { status: 'not_queued' });
|
|
147
|
+
else if (r.status === 'done' && r.cloud_artifact_id) out.set(k, { status: 'done', url: `/api/artifacts/${r.cloud_artifact_id}/raw` });
|
|
148
|
+
else if (r.status === 'failed') out.set(k, { status: 'failed', error: r.last_error ?? 'no error recorded', attempts: r.attempts });
|
|
149
|
+
else if (r.status === 'skipped') out.set(k, { status: 'skipped', reason: r.last_error ?? 'held back' });
|
|
150
|
+
// A retryable failure goes back to 'pending' WITH its error kept (markFailed):
|
|
151
|
+
// that is a failed upload waiting for another try, not an untried one.
|
|
152
|
+
else if (r.last_error) out.set(k, { status: 'retrying', error: r.last_error, attempts: r.attempts });
|
|
153
|
+
else out.set(k, { status: 'pending' });
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
124
158
|
/** Queue several at once — the flow path has one per step. */
|
|
125
159
|
export function enqueueArtifacts(db: Database, projectSlug: string, keys: (string | null)[]): void {
|
|
126
160
|
for (const k of keys) enqueueArtifact(db, projectSlug, k);
|
package/src/flow.ts
CHANGED
|
@@ -2856,7 +2856,8 @@ export async function runFlow(flow: Flow, env: AuthEnv, opts: RunFlowOptions = {
|
|
|
2856
2856
|
// ── leaf step ──
|
|
2857
2857
|
const i = idx;
|
|
2858
2858
|
idx += 1;
|
|
2859
|
-
|
|
2859
|
+
// F009.134 — a loop row reaches the label too («mockup-{{s}}» stayed literal).
|
|
2860
|
+
const label = step.label != null ? subst(step.label, vars) : describe(bindSelectorVars(step, vars));
|
|
2860
2861
|
// F009.122 — which browser this step runs in. `sp` is the page every line
|
|
2861
2862
|
// below acts on; the loop's own `page` is the default browser.
|
|
2862
2863
|
const browserName = (step as { browser?: string }).browser ?? 'main';
|
package/src/service.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
13
13
|
import sharp from 'sharp';
|
|
14
14
|
import { durabilityOf } from './durability.ts';
|
|
15
15
|
import type { Database } from 'bun:sqlite';
|
|
16
|
+
import { cloudCopiesOf } from './artifact-uploads.ts';
|
|
16
17
|
import type { CaptureAction, CaptureMode } from './capture.ts';
|
|
17
18
|
import { coerceStorageSeed } from './seed-storage.ts';
|
|
18
19
|
import type { LensEngine } from './browser.ts';
|
|
@@ -686,8 +687,12 @@ function parseFindingsJson(raw: string | null | undefined, port?: number): Recor
|
|
|
686
687
|
}
|
|
687
688
|
}
|
|
688
689
|
|
|
689
|
-
export function flowRunView(row: LensFlowRunRow, port: number) {
|
|
690
|
-
const
|
|
690
|
+
export function flowRunView(row: LensFlowRunRow, port: number, db?: Database) {
|
|
691
|
+
const stored = JSON.parse(row.steps_json) as StoredFlowStep[];
|
|
692
|
+
// lens F006.1 — each still's cloud copy, read from the upload queue, so the report
|
|
693
|
+
// says where the evidence can be opened from — or why it cannot — per step.
|
|
694
|
+
const clouds = db ? cloudCopiesOf(db, stored.map((s) => s.artifact_key).filter((k): k is string => !!k)) : null;
|
|
695
|
+
const steps = stored.map((s) => ({
|
|
691
696
|
index: s.index,
|
|
692
697
|
type: s.type,
|
|
693
698
|
label: s.label,
|
|
@@ -712,7 +717,11 @@ export function flowRunView(row: LensFlowRunRow, port: number) {
|
|
|
712
717
|
// with internal waiting (a click retrying actionability) answerable at all.
|
|
713
718
|
...(typeof s.ms_step_duration === 'number' ? { ms_step_duration: s.ms_step_duration } : {}),
|
|
714
719
|
screenshot_url: artifactUrl(port, s.artifact_key),
|
|
720
|
+
...(clouds && s.artifact_key ? { cloud: clouds.get(s.artifact_key)! } : {}),
|
|
715
721
|
}));
|
|
722
|
+
// F006.1 — a run whose every still is in the cloud is durable, whatever disk the
|
|
723
|
+
// daemon wrote it to first: the citation no longer depends on this machine.
|
|
724
|
+
const allInCloud = clouds !== null && clouds.size > 0 && [...clouds.values()].every((c) => c.status === 'done');
|
|
716
725
|
return {
|
|
717
726
|
flow_run_id: row.id,
|
|
718
727
|
project: row.project_slug,
|
|
@@ -746,7 +755,7 @@ export function flowRunView(row: LensFlowRunRow, port: number) {
|
|
|
746
755
|
artifact_backend: (row.artifact_backend as 'local' | 'tigris' | null) ?? null,
|
|
747
756
|
// F272.2 — the derived one-word answer to the only question a citation asks:
|
|
748
757
|
// can the owner still open this after this machine is gone?
|
|
749
|
-
evidence_durability: durabilityOf(row.artifact_backend),
|
|
758
|
+
evidence_durability: allInCloud ? 'durable' : durabilityOf(row.artifact_backend),
|
|
750
759
|
// F074.99 — the run-level findings a reader can ACT on: a password that came
|
|
751
760
|
// back in the URL, a page that was never listening while we wrote to it.
|
|
752
761
|
// `{}` means the run looked and found nothing; NULL means the row predates
|
|
@@ -976,7 +985,7 @@ export async function lensRunFlow(deps: LensDeps, input: RunFlowInput) {
|
|
|
976
985
|
storage: coerceStorageSeed(input.storage), // F074.97 — seed saved client state
|
|
977
986
|
});
|
|
978
987
|
const row = await recordFlowRun(deps.db, deps.storage, project, result, provenance);
|
|
979
|
-
return flowRunView(row, deps.port);
|
|
988
|
+
return flowRunView(row, deps.port, deps.db);
|
|
980
989
|
}
|
|
981
990
|
|
|
982
991
|
export interface RunManuscriptInput {
|
|
@@ -1039,7 +1048,7 @@ export async function lensRunManuscript(deps: LensDeps, input: RunManuscriptInpu
|
|
|
1039
1048
|
storage: coerceStorageSeed(input.storage), // F074.97 — seed saved client state
|
|
1040
1049
|
});
|
|
1041
1050
|
const row = await recordFlowRun(deps.db, deps.storage, project, result, provenance);
|
|
1042
|
-
return flowRunView(row, deps.port);
|
|
1051
|
+
return flowRunView(row, deps.port, deps.db);
|
|
1043
1052
|
}
|
|
1044
1053
|
|
|
1045
1054
|
export interface ListFlowRunsInput {
|
|
@@ -1056,7 +1065,7 @@ export function lensListFlowRuns(deps: LensDeps, input: ListFlowRunsInput) {
|
|
|
1056
1065
|
status: input.status,
|
|
1057
1066
|
limit: input.limit,
|
|
1058
1067
|
});
|
|
1059
|
-
return { project, count: runs.length, flow_runs: runs.map((r) => flowRunView(r, deps.port)) };
|
|
1068
|
+
return { project, count: runs.length, flow_runs: runs.map((r) => flowRunView(r, deps.port, deps.db)) };
|
|
1060
1069
|
}
|
|
1061
1070
|
|
|
1062
1071
|
export function lensListBaselines(deps: LensDeps, project: string) {
|