@remnic/core 9.15.2 → 9.16.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/dist/access-admin-ops-surface.d.ts +8 -8
- package/dist/access-admin-ops-surface.js +7 -7
- package/dist/access-boundary.d.ts +8 -8
- package/dist/access-boundary.js +7 -7
- package/dist/access-cli.js +31 -30
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +8 -8
- package/dist/access-http.js +11 -11
- package/dist/access-identity-continuity-surface.d.ts +6 -6
- package/dist/access-identity-continuity-surface.js +7 -7
- package/dist/access-lcm-surface.d.ts +8 -8
- package/dist/access-lcm-surface.js +7 -7
- package/dist/access-mcp.d.ts +8 -8
- package/dist/access-mcp.js +10 -10
- package/dist/access-memory-search-fanout.d.ts +2 -2
- package/dist/{access-namespace-preflight-B9e5GnyZ.d.ts → access-namespace-preflight-DwhEFYy3.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +3 -3
- package/dist/access-namespace-preflight.js +7 -7
- package/dist/access-observe-write-surface.d.ts +8 -8
- package/dist/access-observe-write-surface.js +7 -7
- package/dist/access-operations-batch.js +8 -8
- package/dist/access-operations.d.ts +11 -11
- package/dist/access-operations.js +9 -9
- package/dist/access-recall-concurrency.d.ts +8 -8
- package/dist/access-recall-concurrency.js +7 -7
- package/dist/access-recall-response.d.ts +8 -8
- package/dist/access-recall-response.js +7 -7
- package/dist/access-recall-surface.d.ts +8 -8
- package/dist/access-recall-surface.js +7 -7
- package/dist/access-schema.d.ts +76 -76
- package/dist/{access-service-DgmJd_A-.d.ts → access-service-CSq1Wp2Z.d.ts} +6 -6
- package/dist/access-service.d.ts +8 -8
- package/dist/access-service.js +7 -7
- package/dist/access-surface-catalog.d.ts +8 -8
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +2 -1
- package/dist/active-recall.js.map +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +6 -6
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/bulk-import/index.d.ts +3 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-g_dfT6oL.d.ts → catalog-B9uga4sG.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +4 -4
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-IRX5UCGN.js → chunk-2OH3ZRYH.js} +3 -3
- package/dist/{chunk-YXTSUIXC.js → chunk-5EKOBRCA.js} +6 -1
- package/dist/chunk-5EKOBRCA.js.map +1 -0
- package/dist/{chunk-42DJZZUB.js → chunk-7LXSPIVG.js} +2 -2
- package/dist/{chunk-47WFEKT7.js → chunk-7PK4W4RN.js} +2 -2
- package/dist/{chunk-O7ACJAHR.js → chunk-7YJLCNPF.js} +2 -2
- package/dist/{chunk-6EJL2O3G.js → chunk-AFH7PIFU.js} +2 -2
- package/dist/{chunk-JN4N2TMQ.js → chunk-ANGUOE2N.js} +7 -7
- package/dist/{chunk-GQOKO77V.js → chunk-AWT7SCWQ.js} +2 -2
- package/dist/{chunk-PXJEL55X.js → chunk-BIRPR5N7.js} +2 -2
- package/dist/{chunk-NAY25BGS.js → chunk-BTK2PIFL.js} +6 -6
- package/dist/{chunk-SYCVU556.js → chunk-C2RMQRPN.js} +3 -3
- package/dist/{chunk-T4EAQPQQ.js → chunk-G2WCVFDC.js} +2 -2
- package/dist/{chunk-UJIBXOJA.js → chunk-GEDBTSYU.js} +5 -5
- package/dist/{chunk-CXFVZ3PK.js → chunk-GRJ5UOFO.js} +84 -658
- package/dist/chunk-GRJ5UOFO.js.map +1 -0
- package/dist/chunk-GSQASASV.js +429 -0
- package/dist/chunk-GSQASASV.js.map +1 -0
- package/dist/{chunk-WIKOHCQZ.js → chunk-I6ODVWQB.js} +2 -2
- package/dist/{chunk-H4MDUPER.js → chunk-IZHXSLO7.js} +2 -2
- package/dist/{chunk-RRS5CDB6.js → chunk-JOCXI2O5.js} +2 -2
- package/dist/{chunk-EEUZQQRJ.js → chunk-KGKYTCDO.js} +2 -2
- package/dist/{chunk-JTW4TGPV.js → chunk-L3HAUO7K.js} +2 -2
- package/dist/{chunk-P53HGVJS.js → chunk-LMVAIZR5.js} +2 -2
- package/dist/{chunk-KLXG4BBO.js → chunk-M6TZQDTX.js} +2 -2
- package/dist/{chunk-FJ5IQ3SI.js → chunk-N7DK67GZ.js} +1 -1
- package/dist/chunk-N7DK67GZ.js.map +1 -0
- package/dist/{chunk-U6QWUFSM.js → chunk-OXT5TQDS.js} +2 -2
- package/dist/{chunk-T4OTDMX4.js → chunk-QDTEH6BS.js} +2 -2
- package/dist/{chunk-5MDE5MIC.js → chunk-SESF64QR.js} +2 -2
- package/dist/{chunk-4E5H4FPC.js → chunk-TUNI4GB3.js} +3 -3
- package/dist/{chunk-XJT6LNLY.js → chunk-V6LYAXGW.js} +834 -25
- package/dist/chunk-V6LYAXGW.js.map +1 -0
- package/dist/{chunk-5HFIHWOF.js → chunk-VLIY55Q5.js} +17 -17
- package/dist/{chunk-BNH2VVWP.js → chunk-WIS5JTJR.js} +2 -2
- package/dist/{chunk-OOGGUXAG.js → chunk-WQ2I6ODB.js} +2 -2
- package/dist/{cli-BXCd2cgK.d.ts → cli-DYLGXxuu.d.ts} +5 -5
- package/dist/cli.d.ts +10 -10
- package/dist/cli.js +25 -24
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +3 -3
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +3 -3
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/contradiction/index.d.ts +2 -2
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +6 -6
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/importers/index.d.ts +1 -1
- package/dist/in-flight-reads.d.ts +1 -1
- package/dist/index.d.ts +693 -503
- package/dist/index.js +70 -45
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +8 -8
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +2 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +3 -3
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +3 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/offline-sync-impression-drain.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +9 -8
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +10 -7
- package/dist/orchestration/maintenance.js +7 -6
- package/dist/{orchestrator-BHfWtvEJ.d.ts → orchestrator-BHx-DWCa.d.ts} +7 -5
- package/dist/orchestrator.d.ts +6 -6
- package/dist/orchestrator.js +31 -30
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-CBYcnmDC.d.ts → pipeline-DDHSE876.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/proactive-contention.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{qmd-BIq-jrV4.d.ts → qmd-C4WVh6BM.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- package/dist/recall-concurrency-config.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-memory-map.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +3 -2
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/salvage-envelope.d.ts +1 -1
- package/dist/schemas.d.ts +76 -76
- package/dist/{scope-profiles-DDno-uvy.d.ts → scope-profiles-EqMXwzCz.d.ts} +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-7RIJFDwY.d.ts → semantic-consolidation-CYpvwmQc.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +9 -9
- package/dist/signal.d.ts +1 -1
- package/dist/{storage-DkphZiOJ.d.ts → storage-Dxd7zVZH.d.ts} +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +2 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-timeline-recall.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +66 -66
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-2q_MRFcM.d.ts → types-OUCJALBK.d.ts} +98 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +3 -3
- package/dist/write-envelope.d.ts +1 -1
- package/package.json +2 -2
- package/src/activity/config.test.ts +113 -0
- package/src/activity/config.ts +111 -0
- package/src/activity/digest.ts +2 -2
- package/src/activity/index.ts +6 -0
- package/src/activity/pipeline.test.ts +379 -0
- package/src/activity/pipeline.ts +223 -0
- package/src/activity/reindex.test.ts +97 -0
- package/src/activity/reindex.ts +40 -0
- package/src/activity/runner.test.ts +452 -0
- package/src/activity/runner.ts +237 -0
- package/src/activity/scheduler.test.ts +303 -0
- package/src/activity/scheduler.ts +147 -0
- package/src/activity/source-client.test.ts +208 -0
- package/src/activity/source-client.ts +159 -0
- package/src/activity/sync-registration.test.ts +134 -0
- package/src/activity/sync-registration.ts +139 -0
- package/src/activity/types.ts +17 -0
- package/src/config.test.ts +19 -0
- package/src/config.ts +3 -0
- package/src/orchestration/maintenance.test.ts +42 -42
- package/src/orchestration/maintenance.ts +37 -5
- package/src/orchestration/orchestrator-helpers.ts +36 -1
- package/src/orchestration/recall-internal.ts +6 -6
- package/src/orchestration/recall-search-pipeline.ts +15 -7
- package/src/orchestrator-background-writes.test.ts +2 -2
- package/src/orchestrator.ts +3 -3
- package/src/types.ts +4 -1
- package/dist/chunk-CXFVZ3PK.js.map +0 -1
- package/dist/chunk-FJ5IQ3SI.js.map +0 -1
- package/dist/chunk-XJT6LNLY.js.map +0 -1
- package/dist/chunk-YXTSUIXC.js.map +0 -1
- /package/dist/{chunk-IRX5UCGN.js.map → chunk-2OH3ZRYH.js.map} +0 -0
- /package/dist/{chunk-42DJZZUB.js.map → chunk-7LXSPIVG.js.map} +0 -0
- /package/dist/{chunk-47WFEKT7.js.map → chunk-7PK4W4RN.js.map} +0 -0
- /package/dist/{chunk-O7ACJAHR.js.map → chunk-7YJLCNPF.js.map} +0 -0
- /package/dist/{chunk-6EJL2O3G.js.map → chunk-AFH7PIFU.js.map} +0 -0
- /package/dist/{chunk-JN4N2TMQ.js.map → chunk-ANGUOE2N.js.map} +0 -0
- /package/dist/{chunk-GQOKO77V.js.map → chunk-AWT7SCWQ.js.map} +0 -0
- /package/dist/{chunk-PXJEL55X.js.map → chunk-BIRPR5N7.js.map} +0 -0
- /package/dist/{chunk-NAY25BGS.js.map → chunk-BTK2PIFL.js.map} +0 -0
- /package/dist/{chunk-SYCVU556.js.map → chunk-C2RMQRPN.js.map} +0 -0
- /package/dist/{chunk-T4EAQPQQ.js.map → chunk-G2WCVFDC.js.map} +0 -0
- /package/dist/{chunk-UJIBXOJA.js.map → chunk-GEDBTSYU.js.map} +0 -0
- /package/dist/{chunk-WIKOHCQZ.js.map → chunk-I6ODVWQB.js.map} +0 -0
- /package/dist/{chunk-H4MDUPER.js.map → chunk-IZHXSLO7.js.map} +0 -0
- /package/dist/{chunk-RRS5CDB6.js.map → chunk-JOCXI2O5.js.map} +0 -0
- /package/dist/{chunk-EEUZQQRJ.js.map → chunk-KGKYTCDO.js.map} +0 -0
- /package/dist/{chunk-JTW4TGPV.js.map → chunk-L3HAUO7K.js.map} +0 -0
- /package/dist/{chunk-P53HGVJS.js.map → chunk-LMVAIZR5.js.map} +0 -0
- /package/dist/{chunk-KLXG4BBO.js.map → chunk-M6TZQDTX.js.map} +0 -0
- /package/dist/{chunk-U6QWUFSM.js.map → chunk-OXT5TQDS.js.map} +0 -0
- /package/dist/{chunk-T4OTDMX4.js.map → chunk-QDTEH6BS.js.map} +0 -0
- /package/dist/{chunk-5MDE5MIC.js.map → chunk-SESF64QR.js.map} +0 -0
- /package/dist/{chunk-4E5H4FPC.js.map → chunk-TUNI4GB3.js.map} +0 -0
- /package/dist/{chunk-5HFIHWOF.js.map → chunk-VLIY55Q5.js.map} +0 -0
- /package/dist/{chunk-BNH2VVWP.js.map → chunk-WIS5JTJR.js.map} +0 -0
- /package/dist/{chunk-OOGGUXAG.js.map → chunk-WQ2I6ODB.js.map} +0 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { ACTIVITY_SYNC_DEFAULT_INTERVAL_MS, ActivitySyncScheduler } from "./scheduler.js";
|
|
8
|
+
import { runActivitySyncOnce, type ActivitySyncRunSummary } from "./runner.js";
|
|
9
|
+
import { ActivityStore } from "./store.js";
|
|
10
|
+
import { activityCursorKey } from "./pipeline.js";
|
|
11
|
+
import type { ActivityConfig, ActivitySnapshot, ActivitySourceClient } from "./types.js";
|
|
12
|
+
|
|
13
|
+
/** A hand-driven timer: records armed callbacks + intervals, fires on demand. */
|
|
14
|
+
function fakeTimer(): {
|
|
15
|
+
setTimer: (fn: () => void, ms: number) => number;
|
|
16
|
+
clearTimer: (handle: unknown) => void;
|
|
17
|
+
armed: Array<{ fn: () => void; ms: number }>;
|
|
18
|
+
cleared: number;
|
|
19
|
+
} {
|
|
20
|
+
const armed: Array<{ fn: () => void; ms: number }> = [];
|
|
21
|
+
let cleared = 0;
|
|
22
|
+
return {
|
|
23
|
+
armed,
|
|
24
|
+
get cleared() {
|
|
25
|
+
return cleared;
|
|
26
|
+
},
|
|
27
|
+
setTimer(fn, ms) {
|
|
28
|
+
armed.push({ fn, ms });
|
|
29
|
+
return armed.length;
|
|
30
|
+
},
|
|
31
|
+
clearTimer() {
|
|
32
|
+
cleared += 1;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Drain the microtask queue so a fired tick's async invoke chain settles. */
|
|
38
|
+
function flush(): Promise<void> {
|
|
39
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
40
|
+
setImmediate(resolve);
|
|
41
|
+
return promise;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SUMMARY: ActivitySyncRunSummary = {
|
|
45
|
+
ranAt: "2026-07-22T18:00:00.000Z",
|
|
46
|
+
enabled: true,
|
|
47
|
+
ranCount: 1,
|
|
48
|
+
errorCount: 0,
|
|
49
|
+
reindexErrorCount: 0,
|
|
50
|
+
totalInserted: 0,
|
|
51
|
+
results: [],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function enabledConfig(): ActivityConfig {
|
|
55
|
+
return {
|
|
56
|
+
enabled: true,
|
|
57
|
+
timezone: "UTC",
|
|
58
|
+
syncDays: 1,
|
|
59
|
+
autoSyncIntervalMinutes: 15,
|
|
60
|
+
sources: [{ machineLabel: "workstation-a", baseUrl: "http://127.0.0.1:8760" }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function disabledConfig(): ActivityConfig {
|
|
65
|
+
return {
|
|
66
|
+
enabled: false,
|
|
67
|
+
timezone: "UTC",
|
|
68
|
+
syncDays: 1,
|
|
69
|
+
autoSyncIntervalMinutes: 15,
|
|
70
|
+
sources: [{ machineLabel: "workstation-a", baseUrl: "http://127.0.0.1:8760" }],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test("default-off: start registers no timer and never invokes", async () => {
|
|
75
|
+
const timer = fakeTimer();
|
|
76
|
+
let calls = 0;
|
|
77
|
+
const scheduler = new ActivitySyncScheduler({
|
|
78
|
+
config: disabledConfig(),
|
|
79
|
+
memoryDir: "/tmp/unused-activity-scheduler",
|
|
80
|
+
invoke: async () => {
|
|
81
|
+
calls += 1;
|
|
82
|
+
return SUMMARY;
|
|
83
|
+
},
|
|
84
|
+
setTimer: timer.setTimer,
|
|
85
|
+
clearTimer: timer.clearTimer,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const registration = scheduler.start();
|
|
89
|
+
|
|
90
|
+
assert.equal(registration.registered, false);
|
|
91
|
+
assert.equal(registration.intervalMs, ACTIVITY_SYNC_DEFAULT_INTERVAL_MS);
|
|
92
|
+
assert.equal(timer.armed.length, 0, "no timer armed while disabled");
|
|
93
|
+
await flush();
|
|
94
|
+
assert.equal(calls, 0, "disabled scheduler never invokes the runner");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("enabled: registers a periodic timer that invokes the runner on cadence", async () => {
|
|
98
|
+
const timer = fakeTimer();
|
|
99
|
+
const summaries: ActivitySyncRunSummary[] = [];
|
|
100
|
+
let calls = 0;
|
|
101
|
+
const scheduler = new ActivitySyncScheduler({
|
|
102
|
+
config: enabledConfig(),
|
|
103
|
+
memoryDir: "/tmp/unused-activity-scheduler",
|
|
104
|
+
invoke: async () => {
|
|
105
|
+
calls += 1;
|
|
106
|
+
return SUMMARY;
|
|
107
|
+
},
|
|
108
|
+
setTimer: timer.setTimer,
|
|
109
|
+
clearTimer: timer.clearTimer,
|
|
110
|
+
onRun: (summary) => summaries.push(summary),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const registration = scheduler.start();
|
|
114
|
+
assert.equal(registration.registered, true);
|
|
115
|
+
assert.equal(registration.intervalMs, ACTIVITY_SYNC_DEFAULT_INTERVAL_MS);
|
|
116
|
+
assert.equal(timer.armed.length, 1, "exactly one periodic timer armed");
|
|
117
|
+
assert.equal(timer.armed[0].ms, ACTIVITY_SYNC_DEFAULT_INTERVAL_MS);
|
|
118
|
+
|
|
119
|
+
// Each cadence tick invokes one durable sync pass.
|
|
120
|
+
for (let i = 0; i < 3; i++) {
|
|
121
|
+
timer.armed[0].fn();
|
|
122
|
+
await flush();
|
|
123
|
+
}
|
|
124
|
+
assert.equal(calls, 3, "three ticks -> three sync invocations");
|
|
125
|
+
assert.equal(summaries.length, 3, "each run's summary is surfaced");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("cadence override is honored; default applies when omitted", () => {
|
|
129
|
+
const timer = fakeTimer();
|
|
130
|
+
new ActivitySyncScheduler({
|
|
131
|
+
config: enabledConfig(),
|
|
132
|
+
memoryDir: "/tmp/unused",
|
|
133
|
+
intervalMs: 1_000,
|
|
134
|
+
invoke: async () => SUMMARY,
|
|
135
|
+
setTimer: timer.setTimer,
|
|
136
|
+
clearTimer: timer.clearTimer,
|
|
137
|
+
}).start();
|
|
138
|
+
assert.equal(timer.armed[0].ms, 1_000, "explicit cadence used");
|
|
139
|
+
|
|
140
|
+
// Issue #1899 contracts activity.autoSyncIntervalMinutes default 15, i.e. a
|
|
141
|
+
// 15-minute effective cadence — not connectors' 5-minute poll.
|
|
142
|
+
assert.equal(ACTIVITY_SYNC_DEFAULT_INTERVAL_MS, 15 * 60_000, "default cadence is 15 minutes (#1899)");
|
|
143
|
+
const defaulted = fakeTimer();
|
|
144
|
+
new ActivitySyncScheduler({
|
|
145
|
+
config: enabledConfig(),
|
|
146
|
+
memoryDir: "/tmp/unused",
|
|
147
|
+
invoke: async () => SUMMARY,
|
|
148
|
+
setTimer: defaulted.setTimer,
|
|
149
|
+
clearTimer: defaulted.clearTimer,
|
|
150
|
+
}).start();
|
|
151
|
+
assert.equal(defaulted.armed[0].ms, 900_000, "omitted cadence uses the 15-minute default");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("stop cancels the timer so no later tick invokes", async () => {
|
|
155
|
+
const timer = fakeTimer();
|
|
156
|
+
let calls = 0;
|
|
157
|
+
const scheduler = new ActivitySyncScheduler({
|
|
158
|
+
config: enabledConfig(),
|
|
159
|
+
memoryDir: "/tmp/unused-activity-scheduler",
|
|
160
|
+
invoke: async () => {
|
|
161
|
+
calls += 1;
|
|
162
|
+
return SUMMARY;
|
|
163
|
+
},
|
|
164
|
+
setTimer: timer.setTimer,
|
|
165
|
+
clearTimer: timer.clearTimer,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
scheduler.start();
|
|
169
|
+
timer.armed[0].fn();
|
|
170
|
+
await flush();
|
|
171
|
+
assert.equal(calls, 1);
|
|
172
|
+
|
|
173
|
+
await scheduler.stop();
|
|
174
|
+
assert.equal(timer.cleared, 1, "stop cancels the underlying timer");
|
|
175
|
+
|
|
176
|
+
// A stray callback firing after stop must not invoke (latched-stop guard).
|
|
177
|
+
timer.armed[0].fn();
|
|
178
|
+
await flush();
|
|
179
|
+
assert.equal(calls, 1, "no invocation after stop");
|
|
180
|
+
|
|
181
|
+
// Restart after stop is a no-op: a stopped scheduler stays stopped.
|
|
182
|
+
const restart = scheduler.start();
|
|
183
|
+
assert.equal(restart.registered, false);
|
|
184
|
+
assert.equal(timer.armed.length, 1, "no new timer armed after stop");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("overlapping ticks do not stack: a slow sync blocks the next tick", async () => {
|
|
188
|
+
const timer = fakeTimer();
|
|
189
|
+
let calls = 0;
|
|
190
|
+
let releaseFirst: () => void = () => {};
|
|
191
|
+
const scheduler = new ActivitySyncScheduler({
|
|
192
|
+
config: enabledConfig(),
|
|
193
|
+
memoryDir: "/tmp/unused-activity-scheduler",
|
|
194
|
+
invoke: () => {
|
|
195
|
+
calls += 1;
|
|
196
|
+
const { promise, resolve } = Promise.withResolvers<ActivitySyncRunSummary>();
|
|
197
|
+
releaseFirst = () => resolve(SUMMARY);
|
|
198
|
+
return promise;
|
|
199
|
+
},
|
|
200
|
+
setTimer: timer.setTimer,
|
|
201
|
+
clearTimer: timer.clearTimer,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
scheduler.start();
|
|
205
|
+
timer.armed[0].fn(); // starts a sync that stays in-flight
|
|
206
|
+
await flush();
|
|
207
|
+
timer.armed[0].fn(); // second tick while the first is unresolved
|
|
208
|
+
await flush();
|
|
209
|
+
assert.equal(calls, 1, "the in-flight guard drops the overlapping tick");
|
|
210
|
+
|
|
211
|
+
releaseFirst();
|
|
212
|
+
await flush();
|
|
213
|
+
// Once the first settles, a later tick runs again.
|
|
214
|
+
timer.armed[0].fn();
|
|
215
|
+
await flush();
|
|
216
|
+
assert.equal(calls, 2, "a tick after the sync settles invokes again");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("parser -> scheduler -> durable sync: a tick performs a real durable sync", async () => {
|
|
220
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-activity-scheduler-"));
|
|
221
|
+
const store = ActivityStore.open(memoryDir);
|
|
222
|
+
try {
|
|
223
|
+
const snapshot: ActivitySnapshot = {
|
|
224
|
+
machine: "wire-label",
|
|
225
|
+
capturedAtUtc: "2026-07-22T14:00:00.000Z",
|
|
226
|
+
app: "Browser",
|
|
227
|
+
windowTitle: "Example",
|
|
228
|
+
text: "synthetic snapshot",
|
|
229
|
+
textSource: "ax",
|
|
230
|
+
contentHash: "hash-1",
|
|
231
|
+
};
|
|
232
|
+
const client: ActivitySourceClient = {
|
|
233
|
+
machineLabel: "workstation-a",
|
|
234
|
+
async verify() {
|
|
235
|
+
return { ok: true };
|
|
236
|
+
},
|
|
237
|
+
async fetchSnapshots() {
|
|
238
|
+
return { snapshots: [snapshot], nextCursor: null };
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const timer = fakeTimer();
|
|
243
|
+
const scheduler = new ActivitySyncScheduler({
|
|
244
|
+
config: enabledConfig(),
|
|
245
|
+
memoryDir,
|
|
246
|
+
// Real durable runner, fixture daemon + shared store — the scheduler
|
|
247
|
+
// drives the same code path the default invoke uses.
|
|
248
|
+
invoke: (signal) =>
|
|
249
|
+
runActivitySyncOnce({
|
|
250
|
+
config: enabledConfig(),
|
|
251
|
+
memoryDir,
|
|
252
|
+
store,
|
|
253
|
+
now: new Date("2026-07-22T18:00:00.000Z"),
|
|
254
|
+
signal,
|
|
255
|
+
createSourceClient: () => client,
|
|
256
|
+
}),
|
|
257
|
+
setTimer: timer.setTimer,
|
|
258
|
+
clearTimer: timer.clearTimer,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
scheduler.start();
|
|
262
|
+
timer.armed[0].fn();
|
|
263
|
+
await flush();
|
|
264
|
+
|
|
265
|
+
assert.equal(store.getCursor(activityCursorKey("workstation-a", "2026-07-22")), null, "cursor advanced by the durable sync");
|
|
266
|
+
const rows = store.listSnapshotsForDay(null, "2026-07-22T00:00:00.000Z", "2026-07-23T00:00:00.000Z");
|
|
267
|
+
assert.equal(rows.length, 1, "the tick persisted the snapshot durably");
|
|
268
|
+
} finally {
|
|
269
|
+
store.close();
|
|
270
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("stop aborts an in-flight tick and drains before resolving", async () => {
|
|
275
|
+
const timer = fakeTimer();
|
|
276
|
+
let observedSignal: AbortSignal | undefined;
|
|
277
|
+
let unwound = false;
|
|
278
|
+
const scheduler = new ActivitySyncScheduler({
|
|
279
|
+
config: enabledConfig(),
|
|
280
|
+
memoryDir: "/tmp/unused-activity-scheduler",
|
|
281
|
+
invoke: (signal) =>
|
|
282
|
+
new Promise<ActivitySyncRunSummary>((_resolve, reject) => {
|
|
283
|
+
observedSignal = signal;
|
|
284
|
+
signal?.addEventListener("abort", () => {
|
|
285
|
+
unwound = true;
|
|
286
|
+
reject(new Error("aborted"));
|
|
287
|
+
});
|
|
288
|
+
}),
|
|
289
|
+
setTimer: timer.setTimer,
|
|
290
|
+
clearTimer: timer.clearTimer,
|
|
291
|
+
onError: () => {}, // swallow the abort rejection so the drain resolves
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
scheduler.start();
|
|
295
|
+
timer.armed[0].fn(); // launch a sync that stays in-flight until aborted
|
|
296
|
+
await flush();
|
|
297
|
+
assert.ok(observedSignal, "tick threads an abort signal into invoke");
|
|
298
|
+
assert.equal(observedSignal?.aborted, false, "not aborted while running");
|
|
299
|
+
|
|
300
|
+
await scheduler.stop();
|
|
301
|
+
assert.equal(observedSignal?.aborted, true, "stop aborts the in-flight tick");
|
|
302
|
+
assert.equal(unwound, true, "stop waited for the aborted sync to unwind");
|
|
303
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process periodic driver for activity (screen-capture) sync (issue #1900).
|
|
3
|
+
*
|
|
4
|
+
* The parser (config.ts) validates `config.activity` and the runner
|
|
5
|
+
* (runner.ts) performs one durable sync pass, but nothing drove that pass on a
|
|
6
|
+
* cadence — parsed config alone never synced. This scheduler is the missing
|
|
7
|
+
* link: parser -> scheduler -> durable sync. It registers a repeating timer
|
|
8
|
+
* that invokes the runner, and cancels it on stop.
|
|
9
|
+
*
|
|
10
|
+
* Master default-off: when `config.activity.enabled` is false, `start()`
|
|
11
|
+
* registers no timer and never invokes — source config alone starts nothing.
|
|
12
|
+
* `stop()` clears the timer and latches so no later tick invokes, which is what
|
|
13
|
+
* a runtime teardown (or a config disable + restart) needs. Ticks are
|
|
14
|
+
* overlap-guarded (a slow sync never stacks) and never throw out of the timer.
|
|
15
|
+
*
|
|
16
|
+
* Host-agnostic: the timer and clock are injectable, so a host schedules
|
|
17
|
+
* through this without any OpenClaw import, and tests drive it deterministically.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { runActivitySyncOnce, type ActivitySyncRunSummary } from "./runner.js";
|
|
21
|
+
import type { ActivityConfig } from "./types.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default sync cadence: 15 minutes. Issue #1899 contracts
|
|
25
|
+
* `activity.autoSyncIntervalMinutes` with a default of 15, so the effective
|
|
26
|
+
* default interval is 15 * 60_000 = 900_000 ms. Overridable via `intervalMs`.
|
|
27
|
+
*/
|
|
28
|
+
export const ACTIVITY_SYNC_DEFAULT_INTERVAL_MS = 900_000;
|
|
29
|
+
|
|
30
|
+
/** An opaque timer handle from whichever timer implementation is injected. */
|
|
31
|
+
type TimerHandle = unknown;
|
|
32
|
+
|
|
33
|
+
export interface ActivitySyncSchedulerOptions {
|
|
34
|
+
config: ActivityConfig;
|
|
35
|
+
memoryDir: string;
|
|
36
|
+
/** Cadence in ms; defaults to ACTIVITY_SYNC_DEFAULT_INTERVAL_MS. */
|
|
37
|
+
intervalMs?: number;
|
|
38
|
+
/** The sync pass to run each tick; defaults to the durable runner. */
|
|
39
|
+
invoke?: (signal?: AbortSignal) => Promise<ActivitySyncRunSummary>;
|
|
40
|
+
/** Search-index refresh forwarded to the default runner as `reindexSearch`. */
|
|
41
|
+
reindexSearch?: (signal?: AbortSignal) => Promise<void>;
|
|
42
|
+
/** Timer factory (injectable for tests); defaults to an unref'd setInterval. */
|
|
43
|
+
setTimer?: (fn: () => void, ms: number) => TimerHandle;
|
|
44
|
+
/** Timer canceller matching setTimer; defaults to clearInterval. */
|
|
45
|
+
clearTimer?: (handle: TimerHandle) => void;
|
|
46
|
+
/** Called with each successful run's summary (telemetry/tests). */
|
|
47
|
+
onRun?: (summary: ActivitySyncRunSummary) => void;
|
|
48
|
+
/** Called when a tick's sync rejects; defaults to swallowing (best-effort). */
|
|
49
|
+
onError?: (error: unknown) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ActivitySyncRegistration {
|
|
53
|
+
/** True when a periodic timer was armed (feature enabled). */
|
|
54
|
+
registered: boolean;
|
|
55
|
+
/** The cadence the timer runs at (reported even when not registered). */
|
|
56
|
+
intervalMs: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Registers/cancels a periodic activity sync. One instance per memory dir;
|
|
61
|
+
* `start()` and `stop()` are both idempotent.
|
|
62
|
+
*/
|
|
63
|
+
export class ActivitySyncScheduler {
|
|
64
|
+
private readonly config: ActivityConfig;
|
|
65
|
+
private readonly memoryDir: string;
|
|
66
|
+
private readonly intervalMs: number;
|
|
67
|
+
private readonly invoke: (signal?: AbortSignal) => Promise<ActivitySyncRunSummary>;
|
|
68
|
+
private readonly setTimer: (fn: () => void, ms: number) => TimerHandle;
|
|
69
|
+
private readonly clearTimer: (handle: TimerHandle) => void;
|
|
70
|
+
private readonly onRun: ((summary: ActivitySyncRunSummary) => void) | undefined;
|
|
71
|
+
private readonly onError: ((error: unknown) => void) | undefined;
|
|
72
|
+
|
|
73
|
+
private handle: TimerHandle = null;
|
|
74
|
+
private stopped = false;
|
|
75
|
+
private inFlight = false;
|
|
76
|
+
private inFlightPromise: Promise<void> | null = null;
|
|
77
|
+
private readonly abortController = new AbortController();
|
|
78
|
+
|
|
79
|
+
constructor(options: ActivitySyncSchedulerOptions) {
|
|
80
|
+
this.config = options.config;
|
|
81
|
+
this.memoryDir = options.memoryDir;
|
|
82
|
+
this.intervalMs = options.intervalMs ?? ACTIVITY_SYNC_DEFAULT_INTERVAL_MS;
|
|
83
|
+
this.invoke =
|
|
84
|
+
options.invoke ??
|
|
85
|
+
((signal) =>
|
|
86
|
+
runActivitySyncOnce({
|
|
87
|
+
config: this.config,
|
|
88
|
+
memoryDir: this.memoryDir,
|
|
89
|
+
signal,
|
|
90
|
+
...(options.reindexSearch === undefined ? {} : { reindexSearch: options.reindexSearch }),
|
|
91
|
+
}));
|
|
92
|
+
this.setTimer =
|
|
93
|
+
options.setTimer ??
|
|
94
|
+
((fn, ms) => {
|
|
95
|
+
const timer = setInterval(fn, ms);
|
|
96
|
+
// Never keep the host process alive solely for the sync cadence.
|
|
97
|
+
timer.unref?.();
|
|
98
|
+
return timer;
|
|
99
|
+
});
|
|
100
|
+
this.clearTimer =
|
|
101
|
+
options.clearTimer ?? ((handle) => clearInterval(handle as NodeJS.Timeout));
|
|
102
|
+
this.onRun = options.onRun;
|
|
103
|
+
this.onError = options.onError;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Arm the periodic sync. Disabled config (default-off) registers nothing.
|
|
108
|
+
* Idempotent: a second call while running (or after stop) re-arms nothing.
|
|
109
|
+
*/
|
|
110
|
+
start(): ActivitySyncRegistration {
|
|
111
|
+
if (this.handle !== null || this.stopped || !this.config.enabled) {
|
|
112
|
+
return { registered: this.handle !== null, intervalMs: this.intervalMs };
|
|
113
|
+
}
|
|
114
|
+
this.handle = this.setTimer(() => this.tick(), this.intervalMs);
|
|
115
|
+
return { registered: true, intervalMs: this.intervalMs };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Cancel the periodic sync and abort any in-flight tick, then resolve once it
|
|
120
|
+
* has unwound. The abort + timer-clear + stop-latch happen synchronously, so
|
|
121
|
+
* even a caller that cannot await (a sync teardown) still signals the running
|
|
122
|
+
* sync to stop; async callers can await the returned drain. Idempotent.
|
|
123
|
+
*/
|
|
124
|
+
async stop(): Promise<void> {
|
|
125
|
+
this.stopped = true;
|
|
126
|
+
this.abortController.abort();
|
|
127
|
+
if (this.handle !== null) {
|
|
128
|
+
this.clearTimer(this.handle);
|
|
129
|
+
this.handle = null;
|
|
130
|
+
}
|
|
131
|
+
await this.inFlightPromise?.catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private tick(): void {
|
|
135
|
+
// Latched-stop and overlap guards: a callback that fires after stop(), or
|
|
136
|
+
// while a prior slow sync is still running, must not invoke.
|
|
137
|
+
if (this.stopped || this.inFlight) return;
|
|
138
|
+
this.inFlight = true;
|
|
139
|
+
this.inFlightPromise = this.invoke(this.abortController.signal)
|
|
140
|
+
.then((summary) => this.onRun?.(summary))
|
|
141
|
+
.catch((error) => this.onError?.(error))
|
|
142
|
+
.finally(() => {
|
|
143
|
+
this.inFlight = false;
|
|
144
|
+
this.inFlightPromise = null;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createServer, type RequestListener } from "node:http";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
import { ActivityHttpSourceClient } from "./source-client.js";
|
|
6
|
+
|
|
7
|
+
async function withServer(
|
|
8
|
+
handler: RequestListener,
|
|
9
|
+
run: (baseUrl: string) => Promise<void>,
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
const server = createServer(handler);
|
|
12
|
+
const listening = Promise.withResolvers<void>();
|
|
13
|
+
server.listen(0, "127.0.0.1", listening.resolve);
|
|
14
|
+
await listening.promise;
|
|
15
|
+
try {
|
|
16
|
+
const address = server.address();
|
|
17
|
+
assert.ok(address && typeof address !== "string");
|
|
18
|
+
await run(`http://127.0.0.1:${address.port}`);
|
|
19
|
+
} finally {
|
|
20
|
+
const closed = Promise.withResolvers<void>();
|
|
21
|
+
server.close((error) => (error ? closed.reject(error) : closed.resolve()));
|
|
22
|
+
await closed.promise;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("ActivityHttpSourceClient authenticates and maps a snapshot page", async () => {
|
|
27
|
+
await withServer((request, response) => {
|
|
28
|
+
assert.equal(request.headers.authorization, "Bearer fixture-token");
|
|
29
|
+
assert.equal(request.url, "/v1/snapshots?date=2026-07-22&timezone=America%2FChicago&cursor=next%20cursor");
|
|
30
|
+
response.setHeader("content-type", "application/json");
|
|
31
|
+
response.end(
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
snapshots: [
|
|
34
|
+
{
|
|
35
|
+
capturedAtUtc: "2026-07-22T14:00:00.000Z",
|
|
36
|
+
app: "Browser",
|
|
37
|
+
windowTitle: "Fixture",
|
|
38
|
+
text: "synthetic text",
|
|
39
|
+
textSource: "ax",
|
|
40
|
+
contentHash: "abc123",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
nextCursor: "cursor-2",
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
}, async (baseUrl) => {
|
|
47
|
+
const client = new ActivityHttpSourceClient({ machineLabel: "fixture-machine", baseUrl, token: "fixture-token" });
|
|
48
|
+
const page = await client.fetchSnapshots({ date: "2026-07-22", timezone: "America/Chicago", cursor: "next cursor" });
|
|
49
|
+
|
|
50
|
+
assert.deepEqual(page, {
|
|
51
|
+
snapshots: [
|
|
52
|
+
{
|
|
53
|
+
machine: "fixture-machine",
|
|
54
|
+
capturedAtUtc: "2026-07-22T14:00:00.000Z",
|
|
55
|
+
app: "Browser",
|
|
56
|
+
windowTitle: "Fixture",
|
|
57
|
+
text: "synthetic text",
|
|
58
|
+
textSource: "ax",
|
|
59
|
+
contentHash: "abc123",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
nextCursor: "cursor-2",
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("ActivityHttpSourceClient treats an omitted nextCursor as end-of-pagination", async () => {
|
|
68
|
+
await withServer((_request, response) => {
|
|
69
|
+
response.setHeader("content-type", "application/json");
|
|
70
|
+
// Daemon omits nextCursor entirely (not JSON null) on the final page.
|
|
71
|
+
response.end(
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
snapshots: [
|
|
74
|
+
{
|
|
75
|
+
capturedAtUtc: "2026-07-22T14:00:00.000Z",
|
|
76
|
+
app: "Browser",
|
|
77
|
+
windowTitle: "Fixture",
|
|
78
|
+
text: "synthetic text",
|
|
79
|
+
textSource: "ax",
|
|
80
|
+
contentHash: "abc123",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
}, async (baseUrl) => {
|
|
86
|
+
const client = new ActivityHttpSourceClient({ machineLabel: "fixture-machine", baseUrl });
|
|
87
|
+
const page = await client.fetchSnapshots({ date: "2026-07-22", timezone: "UTC" });
|
|
88
|
+
assert.equal(page.nextCursor, null, "a missing nextCursor is normal completion, not an error");
|
|
89
|
+
assert.equal(page.snapshots.length, 1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("ActivityHttpSourceClient reports an unreachable health probe without treating it as an empty source", async () => {
|
|
94
|
+
await withServer((_request, response) => {
|
|
95
|
+
response.statusCode = 503;
|
|
96
|
+
response.end("temporarily unavailable");
|
|
97
|
+
}, async (baseUrl) => {
|
|
98
|
+
const client = new ActivityHttpSourceClient({ machineLabel: "fixture-machine", baseUrl });
|
|
99
|
+
assert.deepEqual(await client.verify(), { ok: false, detail: "HTTP 503" });
|
|
100
|
+
await assert.rejects(
|
|
101
|
+
client.fetchSnapshots({ date: "2026-07-22", timezone: "UTC" }),
|
|
102
|
+
/HTTP 503/,
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("ActivityHttpSourceClient aborts a stalled request via the default timeout", async () => {
|
|
108
|
+
await withServer((request) => {
|
|
109
|
+
// Never respond: hold the request open so only the client-side timeout can
|
|
110
|
+
// end it. The socket is destroyed when fetch aborts, so teardown proceeds.
|
|
111
|
+
request.resume();
|
|
112
|
+
}, async (baseUrl) => {
|
|
113
|
+
const client = new ActivityHttpSourceClient({ machineLabel: "fixture-machine", baseUrl, timeoutMs: 50 });
|
|
114
|
+
assert.deepEqual((await client.verify()).ok, false);
|
|
115
|
+
await assert.rejects(client.fetchSnapshots({ date: "2026-07-22", timezone: "UTC" }));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("ActivityHttpSourceClient honors a caller-supplied abort signal", async () => {
|
|
120
|
+
await withServer((request) => {
|
|
121
|
+
request.resume();
|
|
122
|
+
}, async (baseUrl) => {
|
|
123
|
+
const client = new ActivityHttpSourceClient({ machineLabel: "fixture-machine", baseUrl });
|
|
124
|
+
await assert.rejects(client.fetchSnapshots({ date: "2026-07-22", timezone: "UTC", signal: AbortSignal.abort() }));
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("ActivityHttpSourceClient rejects a non-positive timeout", () => {
|
|
129
|
+
assert.throws(
|
|
130
|
+
() => new ActivityHttpSourceClient({ machineLabel: "fixture-machine", baseUrl: "http://127.0.0.1:4319", timeoutMs: 0 }),
|
|
131
|
+
/timeoutMs must be a positive number/,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
async function closedLoopbackBaseUrl(): Promise<string> {
|
|
136
|
+
// Reserve an ephemeral loopback port, then close it so the connection is
|
|
137
|
+
// deterministically refused (no reliance on a fixed low port being free).
|
|
138
|
+
const server = createServer();
|
|
139
|
+
const listening = Promise.withResolvers<void>();
|
|
140
|
+
server.listen(0, "127.0.0.1", listening.resolve);
|
|
141
|
+
await listening.promise;
|
|
142
|
+
const address = server.address();
|
|
143
|
+
assert.ok(address && typeof address !== "string");
|
|
144
|
+
const port = address.port;
|
|
145
|
+
const closed = Promise.withResolvers<void>();
|
|
146
|
+
server.close((error) => (error ? closed.reject(error) : closed.resolve()));
|
|
147
|
+
await closed.promise;
|
|
148
|
+
return `http://127.0.0.1:${port}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
test("ActivityHttpSourceClient.verify returns a sanitized detail for network errors", async () => {
|
|
152
|
+
const baseUrl = await closedLoopbackBaseUrl();
|
|
153
|
+
const port = new URL(baseUrl).port;
|
|
154
|
+
const client = new ActivityHttpSourceClient({ machineLabel: "fixture-machine", baseUrl });
|
|
155
|
+
const check = await client.verify();
|
|
156
|
+
assert.equal(check.ok, false);
|
|
157
|
+
assert.ok(check.detail && check.detail.length > 0, "a failure carries some detail");
|
|
158
|
+
assert.ok(!check.detail?.includes("127.0.0.1"), "detail does not echo the host");
|
|
159
|
+
assert.ok(!check.detail?.includes(port), "detail does not echo the port");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("ActivityHttpSourceClient accepts an empty window title and text (untitled/blank window)", async () => {
|
|
163
|
+
await withServer((_request, response) => {
|
|
164
|
+
response.setHeader("content-type", "application/json");
|
|
165
|
+
response.end(
|
|
166
|
+
JSON.stringify({
|
|
167
|
+
snapshots: [
|
|
168
|
+
{
|
|
169
|
+
capturedAtUtc: "2026-07-22T14:00:00.000Z",
|
|
170
|
+
app: "",
|
|
171
|
+
windowTitle: "",
|
|
172
|
+
text: "",
|
|
173
|
+
textSource: "ocr",
|
|
174
|
+
contentHash: "abc123",
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
nextCursor: null,
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
}, async (baseUrl) => {
|
|
181
|
+
const client = new ActivityHttpSourceClient({ machineLabel: "fixture-machine", baseUrl });
|
|
182
|
+
const page = await client.fetchSnapshots({ date: "2026-07-22", timezone: "UTC" });
|
|
183
|
+
// A legitimate no-text/untitled foreground window must parse, not throw —
|
|
184
|
+
// else the runner errors the day and the cursor never advances past it.
|
|
185
|
+
assert.equal(page.snapshots.length, 1);
|
|
186
|
+
assert.equal(page.snapshots[0]?.windowTitle, "");
|
|
187
|
+
assert.equal(page.snapshots[0]?.text, "");
|
|
188
|
+
assert.equal(page.snapshots[0]?.app, "");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("ActivityHttpSourceClient still rejects a missing contentHash identifier", async () => {
|
|
193
|
+
await withServer((_request, response) => {
|
|
194
|
+
response.setHeader("content-type", "application/json");
|
|
195
|
+
response.end(
|
|
196
|
+
JSON.stringify({
|
|
197
|
+
snapshots: [
|
|
198
|
+
{ capturedAtUtc: "2026-07-22T14:00:00.000Z", app: "Browser", windowTitle: "T", text: "x", textSource: "ax" },
|
|
199
|
+
],
|
|
200
|
+
nextCursor: null,
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
}, async (baseUrl) => {
|
|
204
|
+
const client = new ActivityHttpSourceClient({ machineLabel: "fixture-machine", baseUrl });
|
|
205
|
+
// Identity/dedup fields are still required and non-empty.
|
|
206
|
+
await assert.rejects(client.fetchSnapshots({ date: "2026-07-22", timezone: "UTC" }), /contentHash/);
|
|
207
|
+
});
|
|
208
|
+
});
|