@remnic/core 9.16.3 → 9.17.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/access-admin-ops-surface.d.ts +8 -8
- package/dist/access-admin-ops-surface.js +2 -2
- package/dist/access-boundary.d.ts +8 -8
- package/dist/access-boundary.js +2 -2
- package/dist/access-cli.js +14 -14
- package/dist/access-http.d.ts +8 -8
- package/dist/access-http.js +6 -6
- package/dist/access-identity-continuity-surface.d.ts +6 -6
- package/dist/access-identity-continuity-surface.js +2 -2
- package/dist/access-lcm-surface.d.ts +8 -8
- package/dist/access-lcm-surface.js +2 -2
- package/dist/access-mcp.d.ts +8 -8
- package/dist/access-mcp.js +5 -5
- package/dist/access-memory-search-fanout.d.ts +2 -2
- package/dist/{access-namespace-preflight-DwhEFYy3.d.ts → access-namespace-preflight-_yxq3PYy.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +3 -3
- package/dist/access-namespace-preflight.js +2 -2
- package/dist/access-observe-write-surface.d.ts +8 -8
- package/dist/access-observe-write-surface.js +2 -2
- package/dist/access-operations-batch.js +3 -3
- package/dist/access-operations.d.ts +8 -8
- package/dist/access-operations.js +4 -4
- package/dist/access-recall-concurrency.d.ts +8 -8
- package/dist/access-recall-concurrency.js +2 -2
- package/dist/access-recall-response.d.ts +8 -8
- package/dist/access-recall-response.js +2 -2
- package/dist/access-recall-surface.d.ts +8 -8
- package/dist/access-recall-surface.js +2 -2
- package/dist/{access-service-CSq1Wp2Z.d.ts → access-service-CSm242ks.d.ts} +6 -6
- package/dist/access-service.d.ts +8 -8
- package/dist/access-service.js +2 -2
- 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 -2
- package/dist/{auto-sync-UZG3XQZ5.js → auto-sync-IH3G25MT.js} +2 -2
- 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/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-B9uga4sG.d.ts → catalog-9qLG_2OV.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-YHURQ3M5.js → chunk-52EFQMQC.js} +269 -6
- package/dist/chunk-52EFQMQC.js.map +1 -0
- package/dist/{chunk-GEDBTSYU.js → chunk-7O2C2OOF.js} +4 -4
- package/dist/{chunk-OXT5TQDS.js → chunk-B6P3QDBF.js} +2 -2
- package/dist/{chunk-5EKOBRCA.js → chunk-BCNVNUQG.js} +2 -2
- package/dist/{chunk-TUNI4GB3.js → chunk-BGBPDAIN.js} +2 -2
- package/dist/{chunk-V6LYAXGW.js → chunk-F7BWFIDK.js} +2 -2
- package/dist/{chunk-BTK2PIFL.js → chunk-GBT4TFV7.js} +2 -2
- package/dist/{chunk-JOCXI2O5.js → chunk-K2B2TMF3.js} +2 -2
- package/dist/{chunk-VLIY55Q5.js → chunk-KQED7RCV.js} +7 -7
- package/dist/{chunk-GSQASASV.js → chunk-MONWCSVL.js} +65 -4
- package/dist/chunk-MONWCSVL.js.map +1 -0
- package/dist/{chunk-5SB7SC2G.js → chunk-UX3ZXN34.js} +3 -1
- package/dist/{chunk-M6TZQDTX.js → chunk-VZC7ORJE.js} +2 -2
- package/dist/{chunk-ANGUOE2N.js → chunk-WIUYMCPL.js} +2 -2
- package/dist/{chunk-DETNBTZS.js → chunk-XSBUROEB.js} +20 -4
- package/dist/chunk-XSBUROEB.js.map +1 -0
- package/dist/{cli-DYLGXxuu.d.ts → cli-BLZHYY2A.d.ts} +5 -5
- package/dist/cli.d.ts +10 -10
- package/dist/cli.js +11 -11
- package/dist/compounding/engine.d.ts +2 -2
- 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 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- 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-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 +98 -23
- package/dist/index.js +20 -14
- 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/rebuild-memory-lifecycle-ledger.d.ts +2 -2
- 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/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/offline-sync-impression-drain.d.ts +1 -1
- package/dist/offline-sync-impression-drain.js +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +3 -3
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +3 -3
- package/dist/orchestration/maintenance.js +2 -2
- package/dist/{orchestrator-BHx-DWCa.d.ts → orchestrator-CplS9U3U.d.ts} +5 -5
- package/dist/orchestrator.d.ts +6 -6
- package/dist/orchestrator.js +14 -14
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-DDHSE876.d.ts → pipeline-DB4CbFqo.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-C4WVh6BM.d.ts → qmd-BxZmOLHq.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-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-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- 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 -3
- 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/{scope-profiles-EqMXwzCz.d.ts → scope-profiles-NsQQo1Lt.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-CYpvwmQc.d.ts → semantic-consolidation-DW85h30n.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-rule-verifier.d.ts +1 -1
- 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 +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/{storage-Dxd7zVZH.d.ts → storage-o5BTAa8d.d.ts} +1 -1
- package/dist/storage.d.ts +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/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-OUCJALBK.d.ts → types-V3dT-pen.d.ts} +14 -2
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/write-envelope.d.ts +1 -1
- package/package.json +2 -2
- package/src/activity/config.test.ts +42 -4
- package/src/activity/config.ts +83 -8
- package/src/activity/index.ts +5 -3
- package/src/activity/memory-gen.test.ts +436 -0
- package/src/activity/memory-gen.ts +409 -0
- package/src/activity/runner.test.ts +3 -0
- package/src/activity/scheduler.test.ts +3 -0
- package/src/activity/sync-registration.test.ts +2 -1
- package/src/activity/types.ts +15 -1
- package/src/config.test.ts +7 -0
- package/src/offline-sync-impression-drain.test.ts +54 -0
- package/src/offline-sync-impression-drain.ts +35 -3
- package/dist/chunk-DETNBTZS.js.map +0 -1
- package/dist/chunk-GSQASASV.js.map +0 -1
- package/dist/chunk-YHURQ3M5.js.map +0 -1
- /package/dist/{auto-sync-UZG3XQZ5.js.map → auto-sync-IH3G25MT.js.map} +0 -0
- /package/dist/{chunk-GEDBTSYU.js.map → chunk-7O2C2OOF.js.map} +0 -0
- /package/dist/{chunk-OXT5TQDS.js.map → chunk-B6P3QDBF.js.map} +0 -0
- /package/dist/{chunk-5EKOBRCA.js.map → chunk-BCNVNUQG.js.map} +0 -0
- /package/dist/{chunk-TUNI4GB3.js.map → chunk-BGBPDAIN.js.map} +0 -0
- /package/dist/{chunk-V6LYAXGW.js.map → chunk-F7BWFIDK.js.map} +0 -0
- /package/dist/{chunk-BTK2PIFL.js.map → chunk-GBT4TFV7.js.map} +0 -0
- /package/dist/{chunk-JOCXI2O5.js.map → chunk-K2B2TMF3.js.map} +0 -0
- /package/dist/{chunk-VLIY55Q5.js.map → chunk-KQED7RCV.js.map} +0 -0
- /package/dist/{chunk-5SB7SC2G.js.map → chunk-UX3ZXN34.js.map} +0 -0
- /package/dist/{chunk-M6TZQDTX.js.map → chunk-VZC7ORJE.js.map} +0 -0
- /package/dist/{chunk-ANGUOE2N.js.map → chunk-WIUYMCPL.js.map} +0 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { scoreImportance, isAboveImportanceThreshold } from "../importance.js";
|
|
2
|
+
import { getVerdictKind, type JudgeCandidate, type JudgeVerdict, type JudgeVerdictKind } from "../extraction-judge.js";
|
|
3
|
+
import { log } from "../logger.js";
|
|
4
|
+
import type { ExtractedFact, ExtractionResult, ImportanceScore, MemoryStatus } from "../types.js";
|
|
5
|
+
import {
|
|
6
|
+
composeMemoryEnvelope,
|
|
7
|
+
type MemoryWriteInput,
|
|
8
|
+
type SealedMemoryEnvelope,
|
|
9
|
+
type WriteContext,
|
|
10
|
+
} from "../write-envelope.js";
|
|
11
|
+
import { computeTrustScore, decideSmart, type SmartDecision } from "../wearables/trust.js";
|
|
12
|
+
import { activityDayWindow } from "./digest.js";
|
|
13
|
+
import type { ActivityConfig } from "./types.js";
|
|
14
|
+
|
|
15
|
+
const ACTIVITY_ALLOWED_CATEGORIES: Record<string, true> = {
|
|
16
|
+
decision: true,
|
|
17
|
+
commitment: true,
|
|
18
|
+
preference: true,
|
|
19
|
+
moment: true,
|
|
20
|
+
};
|
|
21
|
+
const FIRST_PERSON = /\b(?:i|we|my|our|i['’]ve|we['’]ve)\b/i;
|
|
22
|
+
/**
|
|
23
|
+
* Attribution detection. On-screen text routinely quotes other people, so a
|
|
24
|
+
* first-person pronoun is not ownership evidence. Two shapes are rejected,
|
|
25
|
+
* case-insensitively, with the speaker slot excluding bare first-person
|
|
26
|
+
* pronouns (word-anchored) so "We decided" stays eligible while names that
|
|
27
|
+
* merely begin with a pronoun ("Wendy", "Ian") do not:
|
|
28
|
+
* - verb attribution: "Alice wrote: I decided ..." (leading speaker + verb)
|
|
29
|
+
* - leading label: "Alice: I decided ..." (chat/comment sender headers)
|
|
30
|
+
* Common document labels ("Note:", "TODO:") are not senders and stay eligible.
|
|
31
|
+
*/
|
|
32
|
+
const ATTRIBUTION_SPEAKER = String.raw`(?!(?:i|we|us|me|my|our)\b)[A-Za-z][\w.'’-]*`;
|
|
33
|
+
const SPEECH_VERB =
|
|
34
|
+
String.raw`(?:said|says|wrote|writes|posted|typed|asked|replied|messaged|commented|noted|announced|added|responded|mentioned|told)`;
|
|
35
|
+
const VERB_ATTRIBUTION = new RegExp(String.raw`^\s*${ATTRIBUTION_SPEAKER}\s+${SPEECH_VERB}\b`, "i");
|
|
36
|
+
const LABEL_ATTRIBUTION = new RegExp(
|
|
37
|
+
String.raw`^\s*(${ATTRIBUTION_SPEAKER}(?:\s+[A-Za-z][\w.'’-]*){0,3})\s*:\s*`,
|
|
38
|
+
"i",
|
|
39
|
+
);
|
|
40
|
+
// A sender named inside an otherwise-allowlisted label ("Update from Alice:",
|
|
41
|
+
// "Note by Bob:") is still attribution. Case-sensitive on the capitalized name
|
|
42
|
+
// so generic labels ("Summary by section:") stay eligible.
|
|
43
|
+
const LABEL_NAMES_SENDER = /\b(?:from|by)\s+[A-Z][a-z]/;
|
|
44
|
+
const NON_SENDER_LABELS: Record<string, true> = {
|
|
45
|
+
note: true, notes: true, todo: true, fixme: true, reminder: true, update: true,
|
|
46
|
+
updates: true, warning: true, info: true, tip: true, tips: true, summary: true,
|
|
47
|
+
status: true, tldr: true, fyi: true, idea: true, goal: true, goals: true,
|
|
48
|
+
plan: true, plans: true, action: true, actions: true, context: true, question: true,
|
|
49
|
+
};
|
|
50
|
+
const TRUST_ATTRIBUTE_KEYS: Record<string, true> = {
|
|
51
|
+
trustscore: true,
|
|
52
|
+
trustdecision: true,
|
|
53
|
+
judgeverdict: true,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export interface ActivityMemoryWriter {
|
|
57
|
+
/**
|
|
58
|
+
* Locate an existing activity memory for this day whose normalized content
|
|
59
|
+
* matches, or null. The dedup identity is `(activity day, normalized content)`:
|
|
60
|
+
* `dayStartUtc`/`dayEndUtc` bound the half-open day window and `normalizedKey`
|
|
61
|
+
* is trim+lowercase content — so whitespace/case variants collapse, while the
|
|
62
|
+
* SAME text on a different day is NOT a duplicate (recurring actions persist
|
|
63
|
+
* per day). Enables dedup and in-place promotion instead of duplicate or
|
|
64
|
+
* frozen-pending records.
|
|
65
|
+
*/
|
|
66
|
+
findActivityMemoryByContent(
|
|
67
|
+
normalizedKey: string,
|
|
68
|
+
dayStartUtc: string,
|
|
69
|
+
dayEndUtc: string,
|
|
70
|
+
): Promise<{ id: string; status: MemoryStatus | undefined } | null>;
|
|
71
|
+
/**
|
|
72
|
+
* Count active + pending_review activity memories whose event time falls in
|
|
73
|
+
* the half-open [startUtc, endUtc) day window. Seeds `maxMemoriesPerDay` so
|
|
74
|
+
* the cap holds across repeated digest runs on the same day rather than
|
|
75
|
+
* resetting on every call.
|
|
76
|
+
*/
|
|
77
|
+
countActivityMemoriesForDay(startUtc: string, endUtc: string): Promise<number>;
|
|
78
|
+
/**
|
|
79
|
+
* Promote a pending_review activity memory to active, merging trust evidence.
|
|
80
|
+
* Returns false when the row is missing or no longer pending.
|
|
81
|
+
*/
|
|
82
|
+
promoteActivityMemory(
|
|
83
|
+
id: string,
|
|
84
|
+
attributeUpdates: Record<string, string>,
|
|
85
|
+
confidence?: number,
|
|
86
|
+
): Promise<boolean>;
|
|
87
|
+
/**
|
|
88
|
+
* Demote a pending_review activity memory to rejected on a fresh judge-reject
|
|
89
|
+
* re-verdict. Active rows are never auto-demoted. Returns false when missing
|
|
90
|
+
* or no longer pending.
|
|
91
|
+
*/
|
|
92
|
+
demoteActivityMemory(id: string, attributeUpdates: Record<string, string>): Promise<boolean>;
|
|
93
|
+
writeSealedMemory(
|
|
94
|
+
envelope: SealedMemoryEnvelope,
|
|
95
|
+
extras: { status: MemoryStatus; contentHashSource: string; importance?: ImportanceScore },
|
|
96
|
+
): Promise<{ tombstoneBlocked?: boolean }>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ActivityMemoryGenerationDeps {
|
|
100
|
+
extract(digestBody: string): Promise<ExtractionResult>;
|
|
101
|
+
judge(candidates: JudgeCandidate[]): Promise<Map<number, JudgeVerdict>>;
|
|
102
|
+
writer: ActivityMemoryWriter;
|
|
103
|
+
now?: () => Date;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ActivityMemoryGenerationResult {
|
|
107
|
+
created: number;
|
|
108
|
+
/** Earlier pending_review writes promoted to active by a stronger reassessment. */
|
|
109
|
+
promoted: number;
|
|
110
|
+
/** Earlier pending_review writes retired by a fresh judge-reject verdict. */
|
|
111
|
+
demoted: number;
|
|
112
|
+
pendingReview: number;
|
|
113
|
+
rejectedDisplayedContent: number;
|
|
114
|
+
rejectedByJudge: number;
|
|
115
|
+
skipped: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Activity snapshots contain arbitrary third-party text. Only durable claims
|
|
120
|
+
* explicitly framed as the user's or team's own action, decision, commitment,
|
|
121
|
+
* preference, or moment may enter the trust gate. A first-person pronoun alone
|
|
122
|
+
* is not ownership evidence: quoted or attributed third-party speech (e.g.
|
|
123
|
+
* "Alice wrote: I decided to leave") is rejected.
|
|
124
|
+
*/
|
|
125
|
+
export function isEligibleActivityFact(fact: ExtractionResult["facts"][number]): boolean {
|
|
126
|
+
if (ACTIVITY_ALLOWED_CATEGORIES[fact.category] !== true) return false;
|
|
127
|
+
if (!FIRST_PERSON.test(fact.content)) return false;
|
|
128
|
+
if (VERB_ATTRIBUTION.test(fact.content)) return false;
|
|
129
|
+
const label = LABEL_ATTRIBUTION.exec(fact.content);
|
|
130
|
+
if (label !== null) {
|
|
131
|
+
const labelText = label[1].trim();
|
|
132
|
+
const firstToken = labelText.split(/\s+/)[0].toLowerCase();
|
|
133
|
+
if (NON_SENDER_LABELS[firstToken] !== true || LABEL_NAMES_SENDER.test(labelText)) return false;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Compose an activity envelope in salvage mode (issue #1989 pattern): digest
|
|
140
|
+
* facts are MACHINE-generated, so one malformed optional field must not abort a
|
|
141
|
+
* whole day's writes. Dropped fields are warn-logged, never silent.
|
|
142
|
+
*/
|
|
143
|
+
function composeSalvagedActivityEnvelope(
|
|
144
|
+
input: MemoryWriteInput,
|
|
145
|
+
ctx: WriteContext,
|
|
146
|
+
): SealedMemoryEnvelope {
|
|
147
|
+
const envelope = composeMemoryEnvelope(input, ctx, { salvage: true });
|
|
148
|
+
if (envelope.salvageNotes.length > 0) {
|
|
149
|
+
log.warn(`activity write salvaged invalid fields: ${envelope.salvageNotes.join("; ")}`);
|
|
150
|
+
}
|
|
151
|
+
return envelope;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface WritableCandidate {
|
|
155
|
+
fact: ExtractedFact;
|
|
156
|
+
key: string;
|
|
157
|
+
status: Extract<MemoryStatus, "active" | "pending_review">;
|
|
158
|
+
trust: number;
|
|
159
|
+
trustDecision: string;
|
|
160
|
+
judgeVerdict?: JudgeVerdictKind;
|
|
161
|
+
importance: ImportanceScore;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function generateActivityMemories(
|
|
165
|
+
date: string,
|
|
166
|
+
digestBody: string,
|
|
167
|
+
config: ActivityConfig,
|
|
168
|
+
deps: ActivityMemoryGenerationDeps,
|
|
169
|
+
): Promise<ActivityMemoryGenerationResult> {
|
|
170
|
+
const result: ActivityMemoryGenerationResult = {
|
|
171
|
+
created: 0,
|
|
172
|
+
promoted: 0,
|
|
173
|
+
demoted: 0,
|
|
174
|
+
pendingReview: 0,
|
|
175
|
+
rejectedDisplayedContent: 0,
|
|
176
|
+
rejectedByJudge: 0,
|
|
177
|
+
skipped: 0,
|
|
178
|
+
};
|
|
179
|
+
// `extractionMode` is the independent extraction gate (per the config contract);
|
|
180
|
+
// `enabled` gates source ingestion, not extraction of a supplied digest.
|
|
181
|
+
if (config.extractionMode !== "smart" || digestBody.trim().length === 0) return result;
|
|
182
|
+
|
|
183
|
+
// Event-time bound: the digest's local day. Backfilled or delayed digests
|
|
184
|
+
// then land on the day they describe rather than at write time.
|
|
185
|
+
const { startUtc, endUtc } = activityDayWindow(date, config.timezone);
|
|
186
|
+
|
|
187
|
+
let extracted: ExtractionResult;
|
|
188
|
+
try {
|
|
189
|
+
extracted = await deps.extract(digestBody);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
// A provider/parse failure must not throw out of the pass; return the
|
|
192
|
+
// structured zero-result so the caller can retry on the next digest run.
|
|
193
|
+
log.warn(
|
|
194
|
+
`activity extraction failed; skipping the day's memory pass: ${err instanceof Error ? err.message : String(err)}`,
|
|
195
|
+
);
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
if (extracted.extractionFailure !== undefined) {
|
|
199
|
+
// In-band failure marker (partial/failed provider result): treat like a
|
|
200
|
+
// thrown extraction — warn and return the structured zero-result.
|
|
201
|
+
log.warn(
|
|
202
|
+
`activity extraction reported a failure; skipping the day's memory pass: ${extracted.extractionFailure}`,
|
|
203
|
+
);
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
const candidates = extracted.facts.filter((fact) => {
|
|
207
|
+
if (!isEligibleActivityFact(fact)) {
|
|
208
|
+
result.rejectedDisplayedContent += 1;
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
const importance = scoreImportance(fact.content, fact.category, fact.tags);
|
|
212
|
+
if (
|
|
213
|
+
!Number.isFinite(fact.confidence) ||
|
|
214
|
+
fact.confidence < config.minConfidence ||
|
|
215
|
+
!isAboveImportanceThreshold(importance.level, config.minImportance)
|
|
216
|
+
) {
|
|
217
|
+
result.skipped += 1;
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
});
|
|
222
|
+
let verdicts: Map<number, JudgeVerdict>;
|
|
223
|
+
try {
|
|
224
|
+
verdicts = await deps.judge(candidates.map((fact) => ({
|
|
225
|
+
text: fact.content,
|
|
226
|
+
category: fact.category,
|
|
227
|
+
confidence: fact.confidence,
|
|
228
|
+
tags: fact.tags,
|
|
229
|
+
importanceLevel: scoreImportance(fact.content, fact.category, fact.tags).level,
|
|
230
|
+
})));
|
|
231
|
+
} catch (err) {
|
|
232
|
+
// Degraded smart mode (wearables parity): a judge failure must not abort the
|
|
233
|
+
// day's writes. Fall back to trust scoring alone (no judge accept boost).
|
|
234
|
+
log.warn(
|
|
235
|
+
`activity extraction judge unavailable; using trust scoring only: ${err instanceof Error ? err.message : String(err)}`,
|
|
236
|
+
);
|
|
237
|
+
verdicts = new Map();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Score every candidate once, then keep the STRONGEST (highest-trust) copy per
|
|
241
|
+
// normalized key so a weaker earlier duplicate (e.g. a judge-rejected one)
|
|
242
|
+
// cannot crowd out a stronger later one. The day is fixed for this digest, so
|
|
243
|
+
// the normalized content alone keys the in-run set.
|
|
244
|
+
interface ScoredCandidate {
|
|
245
|
+
fact: ExtractedFact;
|
|
246
|
+
key: string;
|
|
247
|
+
trust: number;
|
|
248
|
+
verdictKind?: JudgeVerdictKind;
|
|
249
|
+
decision: SmartDecision;
|
|
250
|
+
importance: ImportanceScore;
|
|
251
|
+
}
|
|
252
|
+
const strongestByKey = new Map<string, ScoredCandidate>();
|
|
253
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
254
|
+
const fact = candidates[index];
|
|
255
|
+
const verdict = verdicts.get(index);
|
|
256
|
+
const verdictKind = verdict === undefined ? undefined : getVerdictKind(verdict);
|
|
257
|
+
const trust = computeTrustScore({
|
|
258
|
+
extractionConfidence: fact.confidence,
|
|
259
|
+
sourceTrust: config.sourceTrust,
|
|
260
|
+
judgeVerdict: verdictKind,
|
|
261
|
+
evidence: { corroboratedBySources: [] },
|
|
262
|
+
});
|
|
263
|
+
const decision = decideSmart(trust, verdictKind, {
|
|
264
|
+
autoApproveTrust: config.autoApproveTrust,
|
|
265
|
+
reviewTrust: config.reviewTrust,
|
|
266
|
+
});
|
|
267
|
+
// Dedup key: trim + lowercase so whitespace/case variants collapse (day-scoped).
|
|
268
|
+
const key = fact.content.trim().toLowerCase();
|
|
269
|
+
const scored: ScoredCandidate = {
|
|
270
|
+
fact,
|
|
271
|
+
key,
|
|
272
|
+
trust,
|
|
273
|
+
verdictKind,
|
|
274
|
+
decision,
|
|
275
|
+
importance: scoreImportance(fact.content, fact.category, fact.tags),
|
|
276
|
+
};
|
|
277
|
+
const prev = strongestByKey.get(key);
|
|
278
|
+
if (prev === undefined) {
|
|
279
|
+
strongestByKey.set(key, scored);
|
|
280
|
+
} else {
|
|
281
|
+
// In-run duplicate: keep the copy with the more durable decision (active >
|
|
282
|
+
// review > drop), breaking ties by trust — so a higher-confidence reject
|
|
283
|
+
// cannot crowd out a lower-trust accepted copy.
|
|
284
|
+
result.skipped += 1;
|
|
285
|
+
const rankOf = (outcome: SmartDecision["outcome"]): number =>
|
|
286
|
+
outcome === "active" ? 2 : outcome === "review" ? 1 : 0;
|
|
287
|
+
const curRank = rankOf(scored.decision.outcome);
|
|
288
|
+
const prevRank = rankOf(prev.decision.outcome);
|
|
289
|
+
if (curRank > prevRank || (curRank === prevRank && trust > prev.trust)) {
|
|
290
|
+
strongestByKey.set(key, scored);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const writable: WritableCandidate[] = [];
|
|
296
|
+
for (const scored of strongestByKey.values()) {
|
|
297
|
+
const existing = await deps.writer.findActivityMemoryByContent(scored.key, startUtc, endUtc);
|
|
298
|
+
if (existing !== null) {
|
|
299
|
+
// Same-day duplicate of a persisted row: reassess in place; never write new
|
|
300
|
+
// or use a cap slot. (The same text on another day is not found here.)
|
|
301
|
+
if (existing.status === "pending_review") {
|
|
302
|
+
if (scored.decision.outcome === "drop" && scored.decision.reason === "judge-rejected") {
|
|
303
|
+
if (
|
|
304
|
+
await deps.writer.demoteActivityMemory(existing.id, {
|
|
305
|
+
trustScore: scored.trust.toFixed(3),
|
|
306
|
+
trustDecision: "demoted-by-rejection",
|
|
307
|
+
judgeVerdict: "reject",
|
|
308
|
+
})
|
|
309
|
+
) {
|
|
310
|
+
result.demoted += 1;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
} else if (scored.decision.outcome === "active") {
|
|
314
|
+
if (
|
|
315
|
+
await deps.writer.promoteActivityMemory(
|
|
316
|
+
existing.id,
|
|
317
|
+
{
|
|
318
|
+
trustScore: scored.trust.toFixed(3),
|
|
319
|
+
trustDecision: "promoted-by-reassessment",
|
|
320
|
+
...(scored.verdictKind !== undefined ? { judgeVerdict: scored.verdictKind } : {}),
|
|
321
|
+
},
|
|
322
|
+
scored.trust,
|
|
323
|
+
)
|
|
324
|
+
) {
|
|
325
|
+
result.promoted += 1;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
result.skipped += 1;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (scored.decision.outcome === "drop") {
|
|
334
|
+
if (scored.decision.reason === "judge-rejected") result.rejectedByJudge += 1;
|
|
335
|
+
else result.skipped += 1;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
writable.push({
|
|
339
|
+
fact: scored.fact,
|
|
340
|
+
key: scored.key,
|
|
341
|
+
status: scored.decision.outcome === "active" ? "active" : "pending_review",
|
|
342
|
+
trust: scored.trust,
|
|
343
|
+
trustDecision: scored.decision.reason,
|
|
344
|
+
...(scored.verdictKind !== undefined ? { judgeVerdict: scored.verdictKind } : {}),
|
|
345
|
+
importance: scored.importance,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
writable.sort((a, b) => {
|
|
349
|
+
if (a.trust !== b.trust) return b.trust - a.trust;
|
|
350
|
+
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Seed the cap from memories already persisted for this day so repeated digest
|
|
354
|
+
// runs cannot each write up to the limit. `0` stays uncapped.
|
|
355
|
+
const alreadyWritten =
|
|
356
|
+
config.maxMemoriesPerDay > 0 ? await deps.writer.countActivityMemoriesForDay(startUtc, endUtc) : 0;
|
|
357
|
+
let remaining =
|
|
358
|
+
config.maxMemoriesPerDay > 0
|
|
359
|
+
? Math.max(0, config.maxMemoriesPerDay - alreadyWritten)
|
|
360
|
+
: Number.POSITIVE_INFINITY;
|
|
361
|
+
|
|
362
|
+
for (const entry of writable) {
|
|
363
|
+
if (remaining <= 0) {
|
|
364
|
+
result.skipped += 1;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
// Trust fields FIRST so the salvage-mode 64-entry cap can never trim these
|
|
368
|
+
// load-bearing keys; extractor attributes ride along after and never override
|
|
369
|
+
// them (collisions are compared against the canonical lowercase form).
|
|
370
|
+
const structuredAttributes: Record<string, string> = {
|
|
371
|
+
trustScore: entry.trust.toFixed(3),
|
|
372
|
+
trustDecision: entry.trustDecision,
|
|
373
|
+
...(entry.judgeVerdict !== undefined ? { judgeVerdict: entry.judgeVerdict } : {}),
|
|
374
|
+
};
|
|
375
|
+
for (const [key, value] of Object.entries(entry.fact.structuredAttributes ?? {})) {
|
|
376
|
+
if (TRUST_ATTRIBUTE_KEYS[key.trim().toLowerCase()] !== true) structuredAttributes[key] = value;
|
|
377
|
+
}
|
|
378
|
+
const envelope = composeSalvagedActivityEnvelope(
|
|
379
|
+
{
|
|
380
|
+
content: entry.fact.content,
|
|
381
|
+
category: entry.fact.category,
|
|
382
|
+
tags: [...entry.fact.tags, "activity"],
|
|
383
|
+
entityRef: entry.fact.entityRef,
|
|
384
|
+
// Persist the decision-derived trust as confidence; extractor attributes
|
|
385
|
+
// ride along, with trust score/decision + judge verdict added last so
|
|
386
|
+
// downstream sees the active/pending rationale (wearable-path parity).
|
|
387
|
+
confidence: entry.trust,
|
|
388
|
+
validAt: startUtc,
|
|
389
|
+
structuredAttributes,
|
|
390
|
+
sourceConnector: "activity",
|
|
391
|
+
sourceReason: "screen activity digest",
|
|
392
|
+
},
|
|
393
|
+
{ source: "activity", now: deps.now },
|
|
394
|
+
);
|
|
395
|
+
const write = await deps.writer.writeSealedMemory(envelope, {
|
|
396
|
+
status: entry.status,
|
|
397
|
+
contentHashSource: entry.key,
|
|
398
|
+
importance: entry.importance,
|
|
399
|
+
});
|
|
400
|
+
if (write.tombstoneBlocked) {
|
|
401
|
+
result.skipped += 1;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (entry.status === "active") result.created += 1;
|
|
405
|
+
else result.pendingReview += 1;
|
|
406
|
+
remaining -= 1;
|
|
407
|
+
}
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
@@ -8,6 +8,7 @@ import { activityDatabasePath, ActivityStore } from "./store.js";
|
|
|
8
8
|
import { activityDigestPath } from "./digest.js";
|
|
9
9
|
import { runActivitySyncOnce } from "./runner.js";
|
|
10
10
|
import { activityCursorKey } from "./pipeline.js";
|
|
11
|
+
import { defaultActivityConfig } from "./config.js";
|
|
11
12
|
import type {
|
|
12
13
|
ActivityConfig,
|
|
13
14
|
ActivitySnapshot,
|
|
@@ -55,6 +56,7 @@ function fixtureClient(
|
|
|
55
56
|
|
|
56
57
|
function enabledConfig(overrides: Partial<ActivityConfig> = {}): ActivityConfig {
|
|
57
58
|
return {
|
|
59
|
+
...defaultActivityConfig(),
|
|
58
60
|
enabled: true,
|
|
59
61
|
timezone: "UTC",
|
|
60
62
|
syncDays: 1,
|
|
@@ -70,6 +72,7 @@ test("disabled config makes no client, no HTTP call, and no store write", async
|
|
|
70
72
|
let clientBuilt = false;
|
|
71
73
|
const summary = await runActivitySyncOnce({
|
|
72
74
|
config: {
|
|
75
|
+
...defaultActivityConfig(),
|
|
73
76
|
enabled: false,
|
|
74
77
|
timezone: "UTC",
|
|
75
78
|
syncDays: 1,
|
|
@@ -8,6 +8,7 @@ import { ACTIVITY_SYNC_DEFAULT_INTERVAL_MS, ActivitySyncScheduler } from "./sche
|
|
|
8
8
|
import { runActivitySyncOnce, type ActivitySyncRunSummary } from "./runner.js";
|
|
9
9
|
import { ActivityStore } from "./store.js";
|
|
10
10
|
import { activityCursorKey } from "./pipeline.js";
|
|
11
|
+
import { defaultActivityConfig } from "./config.js";
|
|
11
12
|
import type { ActivityConfig, ActivitySnapshot, ActivitySourceClient } from "./types.js";
|
|
12
13
|
|
|
13
14
|
/** A hand-driven timer: records armed callbacks + intervals, fires on demand. */
|
|
@@ -53,6 +54,7 @@ const SUMMARY: ActivitySyncRunSummary = {
|
|
|
53
54
|
|
|
54
55
|
function enabledConfig(): ActivityConfig {
|
|
55
56
|
return {
|
|
57
|
+
...defaultActivityConfig(),
|
|
56
58
|
enabled: true,
|
|
57
59
|
timezone: "UTC",
|
|
58
60
|
syncDays: 1,
|
|
@@ -63,6 +65,7 @@ function enabledConfig(): ActivityConfig {
|
|
|
63
65
|
|
|
64
66
|
function disabledConfig(): ActivityConfig {
|
|
65
67
|
return {
|
|
68
|
+
...defaultActivityConfig(),
|
|
66
69
|
enabled: false,
|
|
67
70
|
timezone: "UTC",
|
|
68
71
|
syncDays: 1,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type ActivitySyncRegistrarDeps,
|
|
9
9
|
type ActivitySyncSchedulerLike,
|
|
10
10
|
} from "./sync-registration.js";
|
|
11
|
+
import { defaultActivityConfig } from "./config.js";
|
|
11
12
|
|
|
12
13
|
class FakeScheduler implements ActivitySyncSchedulerLike {
|
|
13
14
|
starts = 0;
|
|
@@ -33,7 +34,7 @@ function makeDeps(overrides: Partial<ActivitySyncRegistrarDeps> = {}): {
|
|
|
33
34
|
const created: FakeScheduler[] = [];
|
|
34
35
|
const state = { retries: 0 };
|
|
35
36
|
const deps: ActivitySyncRegistrarDeps = {
|
|
36
|
-
config: { enabled: true,
|
|
37
|
+
config: { ...defaultActivityConfig(), enabled: true, sources: [] },
|
|
37
38
|
memoryDir: "/tmp/remnic-activity-registrar",
|
|
38
39
|
qmdCollection: "openclaw-engram",
|
|
39
40
|
secureStoreEnabled: false,
|
package/src/activity/types.ts
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* All timestamps are UTC ISO-8601; day bucketing is half-open [start, end).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import type { ImportanceLevel } from "../types.js";
|
|
15
|
+
|
|
16
|
+
export type ActivityExtractionMode = "off" | "smart";
|
|
17
|
+
|
|
14
18
|
/** One captured on-screen text snapshot (a single window at a single instant). */
|
|
15
19
|
export interface ActivitySnapshot {
|
|
16
20
|
/** Store row id (assigned on insert; absent before persistence). */
|
|
@@ -92,7 +96,7 @@ export interface ActivitySourceConfig {
|
|
|
92
96
|
token?: string;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
/** Opt-in activity synchronization settings. */
|
|
99
|
+
/** Opt-in activity synchronization + trust-gated extraction settings. */
|
|
96
100
|
export interface ActivityConfig {
|
|
97
101
|
enabled: boolean;
|
|
98
102
|
timezone: string;
|
|
@@ -100,4 +104,14 @@ export interface ActivityConfig {
|
|
|
100
104
|
/** Periodic auto-sync cadence in minutes (default 15). */
|
|
101
105
|
autoSyncIntervalMinutes: number;
|
|
102
106
|
sources: ActivitySourceConfig[];
|
|
107
|
+
/** `off` keeps activity searchable only; `smart` trust-gates durable first-person claims. */
|
|
108
|
+
extractionMode: ActivityExtractionMode;
|
|
109
|
+
/** Baseline confidence in activity as a source of the user's own actions. */
|
|
110
|
+
sourceTrust: number;
|
|
111
|
+
autoApproveTrust: number;
|
|
112
|
+
reviewTrust: number;
|
|
113
|
+
minConfidence: number;
|
|
114
|
+
minImportance: ImportanceLevel;
|
|
115
|
+
/** `0` means no count cap. */
|
|
116
|
+
maxMemoriesPerDay: number;
|
|
103
117
|
}
|
package/src/config.test.ts
CHANGED
|
@@ -2365,5 +2365,12 @@ test("parseConfig forwards activity source settings", () => {
|
|
|
2365
2365
|
syncDays: 2,
|
|
2366
2366
|
autoSyncIntervalMinutes: 15,
|
|
2367
2367
|
sources: [{ machineLabel: "fixture-machine", baseUrl: "http://127.0.0.1:4319" }],
|
|
2368
|
+
extractionMode: "off",
|
|
2369
|
+
sourceTrust: 0.6,
|
|
2370
|
+
autoApproveTrust: 0.8,
|
|
2371
|
+
reviewTrust: 0.5,
|
|
2372
|
+
minConfidence: 0.7,
|
|
2373
|
+
minImportance: "normal",
|
|
2374
|
+
maxMemoriesPerDay: 0,
|
|
2368
2375
|
});
|
|
2369
2376
|
});
|
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
drainPendingImpressionsForOfflineSync,
|
|
9
9
|
drainPendingLifecycleForOfflineSync,
|
|
10
10
|
getOfflineSyncStorage,
|
|
11
|
+
listOfflineSyncNamespaces,
|
|
11
12
|
} from "./offline-sync-impression-drain.js";
|
|
13
|
+
import { namespaceIdentityToken } from "./namespaces/identity.js";
|
|
12
14
|
import { isEncryptedFile } from "./secure-store/secure-fs.js";
|
|
13
15
|
import { StorageManager } from "./storage.js";
|
|
14
16
|
|
|
@@ -314,3 +316,55 @@ test("getOfflineSyncStorage never double-folds the root ledger when it is listed
|
|
|
314
316
|
|
|
315
317
|
assert.deepEqual(lifecycleDrains, ["root", "team"], "root ledger is folded exactly once");
|
|
316
318
|
});
|
|
319
|
+
|
|
320
|
+
test("listOfflineSyncNamespaces decodes canonical token dirs and skips names the router would reject", async () => {
|
|
321
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-ns-enum-"));
|
|
322
|
+
try {
|
|
323
|
+
// A project namespace whose canonical token dir `ns-<hex>` exceeds the
|
|
324
|
+
// 64-char route-namespace cap: `getStorage` on the RAW basename throws
|
|
325
|
+
// `unsafe namespace` and 500s the whole root snapshot-stream (the regression).
|
|
326
|
+
const longName = "generalist-project-origin-6d1ad1f2";
|
|
327
|
+
const longToken = namespaceIdentityToken(longName); // ns-<hex>, 71 chars
|
|
328
|
+
assert.ok(longToken.length > 64, "token dir must exceed the route-namespace cap");
|
|
329
|
+
// A namespace whose DECODED name itself exceeds the 64-char route cap:
|
|
330
|
+
// decoding succeeds but isSafeRouteNamespace must still reject it.
|
|
331
|
+
const overCapName = `generalist-project-tag-${"x".repeat(60)}`;
|
|
332
|
+
const overCapToken = namespaceIdentityToken(overCapName);
|
|
333
|
+
assert.ok(overCapName.length > 64, "decoded name must exceed the route cap");
|
|
334
|
+
for (const dir of [longToken, overCapToken, "blend", "ns-default"]) {
|
|
335
|
+
await mkdir(path.join(root, "namespaces", dir, "state"), { recursive: true });
|
|
336
|
+
await writeFile(
|
|
337
|
+
path.join(root, "namespaces", dir, LIFECYCLE_LEDGER_REL),
|
|
338
|
+
"",
|
|
339
|
+
"utf-8",
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const host = {
|
|
343
|
+
config: {
|
|
344
|
+
memoryDir: root,
|
|
345
|
+
defaultNamespace: "generalist",
|
|
346
|
+
sharedNamespace: "shared",
|
|
347
|
+
namespacePolicies: [],
|
|
348
|
+
},
|
|
349
|
+
namespaceCatalog: { listNamespaces: async () => [] },
|
|
350
|
+
} as unknown as Parameters<typeof listOfflineSyncNamespaces>[0];
|
|
351
|
+
|
|
352
|
+
const names = new Set(await listOfflineSyncNamespaces(host));
|
|
353
|
+
|
|
354
|
+
// The canonical token dir is decoded back to its namespace NAME...
|
|
355
|
+
assert.ok(names.has(longName), "canonical token dir must be decoded to its namespace name");
|
|
356
|
+
// ...never surfaced as the raw >64-char token that getStorage would reject.
|
|
357
|
+
assert.ok(!names.has(longToken), "raw over-cap token must never be enumerated as a namespace");
|
|
358
|
+
// A decoded name that is ITSELF over the route cap is skipped entirely
|
|
359
|
+
// (the isSafeRouteNamespace rejection branch).
|
|
360
|
+
assert.ok(!names.has(overCapName), "over-cap decoded name must be skipped");
|
|
361
|
+
assert.ok(!names.has(overCapToken), "over-cap raw token must be skipped");
|
|
362
|
+
// A raw plain-named dir is used verbatim.
|
|
363
|
+
assert.ok(names.has("blend"), "plain namespace dir must be enumerated verbatim");
|
|
364
|
+
// The literal ns-default token decodes to the EMPTY default identity and
|
|
365
|
+
// must map to the configured default namespace, not be skipped as "".
|
|
366
|
+
assert.ok(names.has("generalist"), "ns-default dir maps to the configured default namespace");
|
|
367
|
+
} finally {
|
|
368
|
+
await rm(root, { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
});
|
|
@@ -12,6 +12,8 @@ import { isErrnoCode } from "./utils/errno.js";
|
|
|
12
12
|
import { assertPathInsideRoot, pathIsInside } from "./utils/path-containment.js";
|
|
13
13
|
import { displayErrorDetail } from "./runtime/better-sqlite.js";
|
|
14
14
|
import { getConfiguredNamespaces } from "./scopes/scope-plan.js";
|
|
15
|
+
import { isSafeRouteNamespace } from "./routing/engine.js";
|
|
16
|
+
import { namespaceIdentityFromToken } from "./namespaces/identity.js";
|
|
15
17
|
import type { PluginConfig } from "./types.js";
|
|
16
18
|
|
|
17
19
|
type PendingImpressionDrain = () => Promise<{ pendingDeferred: boolean }>;
|
|
@@ -89,7 +91,17 @@ export async function getOfflineSyncStorage<T extends PendingLifecycleDrain & {
|
|
|
89
91
|
if (path.resolve(storage.dir) === memoryRoot && orchestrator.listOfflineSyncNamespaces) {
|
|
90
92
|
const drained = new Set<string>([memoryRoot]);
|
|
91
93
|
for (const ns of await orchestrator.listOfflineSyncNamespaces()) {
|
|
92
|
-
|
|
94
|
+
// A single unresolvable namespace (e.g. a legacy canonical token dir whose
|
|
95
|
+
// name exceeds the route-namespace length cap) must not abort the whole root
|
|
96
|
+
// snapshot — degrade to draining the rest. listOfflineSyncNamespaces already
|
|
97
|
+
// normalizes token dirs, so this is defense-in-depth.
|
|
98
|
+
let nsStorage: T;
|
|
99
|
+
try {
|
|
100
|
+
nsStorage = await orchestrator.getStorage(ns);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
log.debug(`offline-sync root drain: skipping namespace ${ns} (non-fatal): ${err}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
93
105
|
const nsDir = path.resolve(nsStorage.dir);
|
|
94
106
|
if (drained.has(nsDir)) continue;
|
|
95
107
|
drained.add(nsDir);
|
|
@@ -114,8 +126,28 @@ export async function listOfflineSyncNamespaces(host: OfflineSyncNamespaceHost):
|
|
|
114
126
|
try {
|
|
115
127
|
for (const ledgerPath of await offlineSyncLifecycleLedgerPaths(host.config.memoryDir)) {
|
|
116
128
|
const namespaceDir = path.dirname(path.dirname(ledgerPath));
|
|
117
|
-
if (path.basename(path.dirname(namespaceDir))
|
|
118
|
-
|
|
129
|
+
if (path.basename(path.dirname(namespaceDir)) !== "namespaces") continue;
|
|
130
|
+
// A namespace is stored under its canonical token dir `ns-<hex>`. Decode the
|
|
131
|
+
// token back to the namespace NAME so the drain fanout resolves the SAME
|
|
132
|
+
// storage the token dir belongs to (the catalog rebuild scanner decodes the
|
|
133
|
+
// same way, catalog.ts finishRebuild). A raw dir already named as a plain
|
|
134
|
+
// namespace is used verbatim. Skip any name the router would reject — e.g. a
|
|
135
|
+
// canonical token dir whose decoded-or-raw name exceeds isSafeRouteNamespace's
|
|
136
|
+
// 64-char cap — because getStorage() would throw `unsafe namespace` and 500
|
|
137
|
+
// the whole root snapshot-stream (the offline-sync regression this fixes).
|
|
138
|
+
const dirName = path.basename(namespaceDir);
|
|
139
|
+
// namespaceIdentityFromToken returns "" for the ns-default token (the
|
|
140
|
+
// default-namespace identity) — map that to the configured default
|
|
141
|
+
// namespace instead of skipping the directory as an unsafe empty name.
|
|
142
|
+
const decoded = namespaceIdentityFromToken(dirName);
|
|
143
|
+
const name =
|
|
144
|
+
decoded === null
|
|
145
|
+
? dirName
|
|
146
|
+
: decoded.length > 0
|
|
147
|
+
? decoded
|
|
148
|
+
: host.config.defaultNamespace;
|
|
149
|
+
if (isSafeRouteNamespace(name)) {
|
|
150
|
+
names.add(name);
|
|
119
151
|
}
|
|
120
152
|
}
|
|
121
153
|
} catch {
|