@broberg/lens-daemon 0.1.30 → 0.1.33

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@broberg/lens-daemon",
3
- "version": "0.1.30",
3
+ "version": "0.1.33",
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",
@@ -12,11 +12,11 @@
12
12
  "pixelmatch": "^6.0.0",
13
13
  "playwright": "1.61.1",
14
14
  "pngjs": "^7.0.0",
15
- "sharp": "^0.33.5",
15
+ "sharp": "^0.35.5",
16
16
  "turndown": "^7.2.4",
17
17
  "zod": "^3.23.8",
18
- "@broberg/lens-engine": "0.14.0",
19
- "@broberg/lens-svg": "0.1.0"
18
+ "@broberg/lens-svg": "0.1.1",
19
+ "@broberg/lens-engine": "0.14.1"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@happy-dom/global-registrator": "^20.10.6",
@@ -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/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 steps = (JSON.parse(row.steps_json) as StoredFlowStep[]).map((s) => ({
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) {