@remnic/core 9.3.624 → 9.3.625
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-cli.js +18 -16
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +12 -5
- package/dist/access-http.js +10 -9
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +8 -8
- package/dist/access-schema.d.ts +5 -5
- package/dist/{access-service-CBNEKjzN.d.ts → access-service-C_sfOHsX.d.ts} +26 -3
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +7 -7
- 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 +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.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/{chunk-7TPH6UZL.js → chunk-2RHI3FGV.js} +540 -17
- package/dist/chunk-2RHI3FGV.js.map +1 -0
- package/dist/{chunk-GYTVOLNX.js → chunk-3MNBW7R7.js} +2 -2
- package/dist/{chunk-QFQQFX2H.js → chunk-3R2UZV3U.js} +2 -2
- package/dist/{chunk-O4UNM6OR.js → chunk-532VCWYW.js} +2 -2
- package/dist/{chunk-2UFQYU5F.js → chunk-57QXN2CS.js} +2 -2
- package/dist/chunk-7WV3F5DQ.js +22 -0
- package/dist/chunk-7WV3F5DQ.js.map +1 -0
- package/dist/{chunk-RKW6QR7W.js → chunk-AZ4RI3QD.js} +1461 -78
- package/dist/chunk-AZ4RI3QD.js.map +1 -0
- package/dist/{chunk-KQFQ3IS5.js → chunk-F3FY3D3S.js} +43 -7
- package/dist/chunk-F3FY3D3S.js.map +1 -0
- package/dist/{chunk-4R4KTDIE.js → chunk-FPNQF475.js} +1 -1
- package/dist/chunk-FPNQF475.js.map +1 -0
- package/dist/{chunk-UGEBPVNI.js → chunk-GE7Q7KXP.js} +2 -2
- package/dist/{chunk-GLWW3EJQ.js → chunk-KB4MFBF5.js} +3 -3
- package/dist/{chunk-5GOMXHLC.js → chunk-KKTXCFD7.js} +255 -1
- package/dist/chunk-KKTXCFD7.js.map +1 -0
- package/dist/{chunk-FH3PPO42.js → chunk-KVFYTRMV.js} +2 -2
- package/dist/{chunk-BNW5NJJH.js → chunk-LQYTQCXM.js} +2 -2
- package/dist/{chunk-AYHXQR53.js → chunk-MVQN73GT.js} +2 -2
- package/dist/{chunk-ZZPIJPPD.js → chunk-N5RGXWLQ.js} +2 -2
- package/dist/chunk-NDAH7BJ5.js +213 -0
- package/dist/chunk-NDAH7BJ5.js.map +1 -0
- package/dist/{chunk-R3OQGYOU.js → chunk-P2D2MM47.js} +2 -2
- package/dist/{chunk-PSUB67YB.js → chunk-PW6GURU3.js} +2 -2
- package/dist/{chunk-W3BKVM64.js → chunk-QDV6VAD4.js} +2 -2
- package/dist/{chunk-3QSU4NFF.js → chunk-QHXW3LZV.js} +3 -3
- package/dist/{chunk-I6UCUHLK.js → chunk-SHV5Y2WU.js} +182 -3
- package/dist/chunk-SHV5Y2WU.js.map +1 -0
- package/dist/{chunk-OZXVGYGZ.js → chunk-STDAAGH7.js} +2 -2
- package/dist/{chunk-FMGWXIES.js → chunk-TZDSNIRO.js} +5 -5
- package/dist/{chunk-2L54V4ZO.js → chunk-UELS6WWF.js} +2 -2
- package/dist/{chunk-PJGB7XRR.js → chunk-UGHUNQ74.js} +502 -134
- package/dist/chunk-UGHUNQ74.js.map +1 -0
- package/dist/{chunk-FG76RDVI.js → chunk-Y3TMFC6I.js} +136 -4
- package/dist/chunk-Y3TMFC6I.js.map +1 -0
- package/dist/{chunk-BPSGLMQ4.js → chunk-YQNADJCT.js} +2 -2
- package/dist/{cli-Cw729yLf.d.ts → cli-EZv6YE6_.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +23 -21
- package/dist/compounding/engine.d.ts +1 -1
- 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 +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- 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 +1 -1
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- 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/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +307 -9
- package/dist/index.js +155 -29
- package/dist/index.js.map +1 -1
- 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/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- 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 +5 -5
- 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 +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- 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 +1 -1
- package/dist/namespaces/storage.js +3 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +8 -7
- package/dist/{orchestrator-CqWOjfgl.d.ts → orchestrator-CEycaY3M.d.ts} +361 -4
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +13 -11
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.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-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-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/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +3 -2
- package/dist/retrieval-agents.d.ts +1 -1
- 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/schemas.d.ts +10 -10
- 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-SLAa_prH.d.ts → semantic-DJR8_DMQ.d.ts} +1 -1
- package/dist/{semantic-consolidation-4HkHWgeI.d.ts → semantic-consolidation-FbhPeJjB.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 +5 -5
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +19 -1
- 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 +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/types-D5VRAI04.d.ts +3134 -0
- package/dist/types.d.ts +3 -2862
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +3 -3
- package/package.json +1 -1
- package/src/access-http.ts +167 -0
- package/src/access-mcp.ts +198 -0
- package/src/access-service.ts +65 -0
- package/src/cli.ts +187 -0
- package/src/config.ts +7 -0
- package/src/index.ts +7 -0
- package/src/orchestrator.ts +42 -0
- package/src/storage.ts +106 -0
- package/src/types.ts +5 -0
- package/src/wearables/cleanup.test.ts +134 -0
- package/src/wearables/cleanup.ts +188 -0
- package/src/wearables/cli.test.ts +170 -0
- package/src/wearables/cli.ts +441 -0
- package/src/wearables/config.test.ts +143 -0
- package/src/wearables/config.ts +332 -0
- package/src/wearables/corrections.test.ts +118 -0
- package/src/wearables/corrections.ts +211 -0
- package/src/wearables/day-store.test.ts +143 -0
- package/src/wearables/day-store.ts +238 -0
- package/src/wearables/errors.test.ts +32 -0
- package/src/wearables/errors.ts +29 -0
- package/src/wearables/index.ts +114 -0
- package/src/wearables/memory-gen.test.ts +342 -0
- package/src/wearables/memory-gen.ts +413 -0
- package/src/wearables/pipeline.test.ts +608 -0
- package/src/wearables/pipeline.ts +519 -0
- package/src/wearables/redaction.test.ts +94 -0
- package/src/wearables/redaction.ts +156 -0
- package/src/wearables/registry.test.ts +62 -0
- package/src/wearables/registry.ts +133 -0
- package/src/wearables/service.test.ts +425 -0
- package/src/wearables/service.ts +691 -0
- package/src/wearables/speakers.test.ts +110 -0
- package/src/wearables/speakers.ts +174 -0
- package/src/wearables/storage-io.test.ts +105 -0
- package/src/wearables/sync-state.test.ts +134 -0
- package/src/wearables/sync-state.ts +186 -0
- package/src/wearables/types.ts +285 -0
- package/dist/chunk-4R4KTDIE.js.map +0 -1
- package/dist/chunk-5GOMXHLC.js.map +0 -1
- package/dist/chunk-7TPH6UZL.js.map +0 -1
- package/dist/chunk-FG76RDVI.js.map +0 -1
- package/dist/chunk-I6UCUHLK.js.map +0 -1
- package/dist/chunk-KQFQ3IS5.js.map +0 -1
- package/dist/chunk-PJGB7XRR.js.map +0 -1
- package/dist/chunk-RKW6QR7W.js.map +0 -1
- /package/dist/{chunk-GYTVOLNX.js.map → chunk-3MNBW7R7.js.map} +0 -0
- /package/dist/{chunk-QFQQFX2H.js.map → chunk-3R2UZV3U.js.map} +0 -0
- /package/dist/{chunk-O4UNM6OR.js.map → chunk-532VCWYW.js.map} +0 -0
- /package/dist/{chunk-2UFQYU5F.js.map → chunk-57QXN2CS.js.map} +0 -0
- /package/dist/{chunk-UGEBPVNI.js.map → chunk-GE7Q7KXP.js.map} +0 -0
- /package/dist/{chunk-GLWW3EJQ.js.map → chunk-KB4MFBF5.js.map} +0 -0
- /package/dist/{chunk-FH3PPO42.js.map → chunk-KVFYTRMV.js.map} +0 -0
- /package/dist/{chunk-BNW5NJJH.js.map → chunk-LQYTQCXM.js.map} +0 -0
- /package/dist/{chunk-AYHXQR53.js.map → chunk-MVQN73GT.js.map} +0 -0
- /package/dist/{chunk-ZZPIJPPD.js.map → chunk-N5RGXWLQ.js.map} +0 -0
- /package/dist/{chunk-R3OQGYOU.js.map → chunk-P2D2MM47.js.map} +0 -0
- /package/dist/{chunk-PSUB67YB.js.map → chunk-PW6GURU3.js.map} +0 -0
- /package/dist/{chunk-W3BKVM64.js.map → chunk-QDV6VAD4.js.map} +0 -0
- /package/dist/{chunk-3QSU4NFF.js.map → chunk-QHXW3LZV.js.map} +0 -0
- /package/dist/{chunk-OZXVGYGZ.js.map → chunk-STDAAGH7.js.map} +0 -0
- /package/dist/{chunk-FMGWXIES.js.map → chunk-TZDSNIRO.js.map} +0 -0
- /package/dist/{chunk-2L54V4ZO.js.map → chunk-UELS6WWF.js.map} +0 -0
- /package/dist/{chunk-BPSGLMQ4.js.map → chunk-YQNADJCT.js.map} +0 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wearable sync pipeline — pull → clean → label → correct → store →
|
|
3
|
+
* (optionally) remember, for one source.
|
|
4
|
+
*
|
|
5
|
+
* Stage order per conversation is deliberate:
|
|
6
|
+
* 1. off-the-record elision (before anything can persist the span)
|
|
7
|
+
* 2. cleanup (merging first lets redaction see numbers
|
|
8
|
+
* that ASR split across segments)
|
|
9
|
+
* 3. redaction (built-in + user patterns)
|
|
10
|
+
* 4. corrections (user-specific word fixes)
|
|
11
|
+
*
|
|
12
|
+
* Day files are rebuilt idempotently; the per-day body hash recorded in
|
|
13
|
+
* sync state lets unchanged days skip both the rewrite and the
|
|
14
|
+
* (expensive) memory extraction. Sync state advances only after every
|
|
15
|
+
* write for the run has succeeded.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { cleanConversation } from "./cleanup.js";
|
|
19
|
+
import { describeErrorForOperator, WearablesInputError } from "./errors.js";
|
|
20
|
+
import {
|
|
21
|
+
applyCorrections,
|
|
22
|
+
compileCorrectionRules,
|
|
23
|
+
loadCorrectionsFile,
|
|
24
|
+
type CompiledCorrectionRule,
|
|
25
|
+
} from "./corrections.js";
|
|
26
|
+
import {
|
|
27
|
+
composeDayTranscriptBody,
|
|
28
|
+
composeDayTranscriptMeta,
|
|
29
|
+
hashTranscriptBody,
|
|
30
|
+
isValidTranscriptDate,
|
|
31
|
+
serializeDayTranscript,
|
|
32
|
+
} from "./day-store.js";
|
|
33
|
+
import {
|
|
34
|
+
generateWearableMemories,
|
|
35
|
+
importNativeMemories,
|
|
36
|
+
writeDailyDigestMemory,
|
|
37
|
+
type WearableMemoryGenDeps,
|
|
38
|
+
} from "./memory-gen.js";
|
|
39
|
+
import { applyOffTheRecord, compileRedactionPatterns, redactText } from "./redaction.js";
|
|
40
|
+
import { loadSpeakerRegistry } from "./speakers.js";
|
|
41
|
+
import {
|
|
42
|
+
loadSyncState,
|
|
43
|
+
saveSyncState,
|
|
44
|
+
updateSourceSyncState,
|
|
45
|
+
} from "./sync-state.js";
|
|
46
|
+
import type {
|
|
47
|
+
WearableConversation,
|
|
48
|
+
WearableSourceConnector,
|
|
49
|
+
WearableSourceSettings,
|
|
50
|
+
WearableSyncSummary,
|
|
51
|
+
WearablesConfig,
|
|
52
|
+
} from "./types.js";
|
|
53
|
+
|
|
54
|
+
/** Safety cap on pages fetched per day window. */
|
|
55
|
+
const MAX_PAGES_PER_DAY = 50;
|
|
56
|
+
/** Safety cap on native-memory pages per sync. */
|
|
57
|
+
const MAX_NATIVE_PAGES = 20;
|
|
58
|
+
/** Default lookback window (today + yesterday) for unscoped syncs. */
|
|
59
|
+
const DEFAULT_SYNC_DAYS = 2;
|
|
60
|
+
const MAX_SYNC_DAYS = 90;
|
|
61
|
+
|
|
62
|
+
export interface WearableSyncOptions {
|
|
63
|
+
/** Sync exactly this day (YYYY-MM-DD). Overrides `days`. */
|
|
64
|
+
date?: string;
|
|
65
|
+
/** Lookback window in days ending today (default 2, max 90). */
|
|
66
|
+
days?: number;
|
|
67
|
+
/** Re-run memory extraction even for unchanged days. */
|
|
68
|
+
forceMemories?: boolean;
|
|
69
|
+
signal?: AbortSignal;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface WearableSyncDeps {
|
|
73
|
+
/** Memory dir for state files (speakers, corrections, sync ledger). */
|
|
74
|
+
memoryDir: string;
|
|
75
|
+
/** Read the stored content hash for a day file, if present. */
|
|
76
|
+
readDayContentHash(sourceId: string, date: string): Promise<string | null>;
|
|
77
|
+
/** Persist a serialized day-transcript file (atomic). */
|
|
78
|
+
writeDayTranscript(
|
|
79
|
+
sourceId: string,
|
|
80
|
+
date: string,
|
|
81
|
+
serialized: string,
|
|
82
|
+
): Promise<void>;
|
|
83
|
+
/** Optional hook fired once after any day files changed (reindex). */
|
|
84
|
+
afterTranscriptsWritten?(): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Memory-generation dependencies, or null when no extraction engine
|
|
87
|
+
* is available in this context (transcripts still sync; memory
|
|
88
|
+
* creation is skipped with a warning when the mode wanted it).
|
|
89
|
+
*/
|
|
90
|
+
memoryGen: WearableMemoryGenDeps | null;
|
|
91
|
+
/** Clock injection for tests. */
|
|
92
|
+
now?: () => Date;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Format a Date as YYYY-MM-DD in the given IANA timezone. */
|
|
96
|
+
export function dateInTimezone(date: Date, timezone: string): string {
|
|
97
|
+
try {
|
|
98
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
99
|
+
timeZone: timezone,
|
|
100
|
+
year: "numeric",
|
|
101
|
+
month: "2-digit",
|
|
102
|
+
day: "2-digit",
|
|
103
|
+
}).formatToParts(date);
|
|
104
|
+
const get = (type: string) =>
|
|
105
|
+
parts.find((part) => part.type === type)?.value ?? "";
|
|
106
|
+
return `${get("year")}-${get("month")}-${get("day")}`;
|
|
107
|
+
} catch {
|
|
108
|
+
return date.toISOString().slice(0, 10);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Resolve the list of days to sync, oldest first. */
|
|
113
|
+
export function resolveSyncDates(
|
|
114
|
+
options: WearableSyncOptions,
|
|
115
|
+
timezone: string,
|
|
116
|
+
now: Date,
|
|
117
|
+
): string[] {
|
|
118
|
+
if (options.date !== undefined) {
|
|
119
|
+
if (!isValidTranscriptDate(options.date)) {
|
|
120
|
+
throw new WearablesInputError(
|
|
121
|
+
`wearables sync: invalid date '${options.date}' — expected YYYY-MM-DD`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return [options.date];
|
|
125
|
+
}
|
|
126
|
+
let days = DEFAULT_SYNC_DAYS;
|
|
127
|
+
if (options.days !== undefined) {
|
|
128
|
+
if (
|
|
129
|
+
!Number.isFinite(options.days) ||
|
|
130
|
+
!Number.isInteger(options.days) ||
|
|
131
|
+
options.days < 1 ||
|
|
132
|
+
options.days > MAX_SYNC_DAYS
|
|
133
|
+
) {
|
|
134
|
+
throw new WearablesInputError(
|
|
135
|
+
`wearables sync: invalid days '${options.days}' — expected an integer between 1 and ${MAX_SYNC_DAYS}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
days = options.days;
|
|
139
|
+
}
|
|
140
|
+
// Walk back by CALENDAR days from today's local date — subtracting
|
|
141
|
+
// fixed 24h intervals from the wall clock can skip a local day
|
|
142
|
+
// around DST transitions (Codex P2 on PR #1458).
|
|
143
|
+
const dates: string[] = [];
|
|
144
|
+
let cursor = dateInTimezone(now, timezone);
|
|
145
|
+
for (let count = 0; count < days; count++) {
|
|
146
|
+
dates.unshift(cursor);
|
|
147
|
+
cursor = previousIsoDate(cursor);
|
|
148
|
+
}
|
|
149
|
+
return dates;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Previous calendar date in pure date arithmetic (no DST exposure). */
|
|
153
|
+
function previousIsoDate(date: string): string {
|
|
154
|
+
const parsed = new Date(`${date}T00:00:00Z`);
|
|
155
|
+
parsed.setUTCDate(parsed.getUTCDate() - 1);
|
|
156
|
+
return parsed.toISOString().slice(0, 10);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function fetchAllConversationsForDate(
|
|
160
|
+
connector: WearableSourceConnector,
|
|
161
|
+
date: string,
|
|
162
|
+
timezone: string,
|
|
163
|
+
signal: AbortSignal | undefined,
|
|
164
|
+
warnings: string[],
|
|
165
|
+
): Promise<{ conversations: WearableConversation[]; partial: boolean }> {
|
|
166
|
+
const conversations: WearableConversation[] = [];
|
|
167
|
+
let cursor: string | null | undefined = undefined;
|
|
168
|
+
for (let page = 0; page < MAX_PAGES_PER_DAY; page++) {
|
|
169
|
+
const result = await connector.fetchConversations({
|
|
170
|
+
date,
|
|
171
|
+
timezone,
|
|
172
|
+
cursor,
|
|
173
|
+
signal,
|
|
174
|
+
});
|
|
175
|
+
conversations.push(...result.conversations);
|
|
176
|
+
if (!result.nextCursor) return { conversations, partial: false };
|
|
177
|
+
cursor = result.nextCursor;
|
|
178
|
+
}
|
|
179
|
+
warnings.push(
|
|
180
|
+
`${connector.id}: stopped paginating ${date} after ${MAX_PAGES_PER_DAY} pages — day may be partially synced (every sync refetches and re-warns until the provider day fits the cap)`,
|
|
181
|
+
);
|
|
182
|
+
return { conversations, partial: true };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Visible marker appended to day files whose fetch hit the page cap. */
|
|
186
|
+
const PARTIAL_DAY_MARKER =
|
|
187
|
+
"\n*Note: pagination safety cap reached during sync — this day may be incomplete.*\n";
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Explicit replacement body for a day whose provider data exists but
|
|
191
|
+
* whose every segment was elided or dropped (all off-the-record, all
|
|
192
|
+
* ASR garbage). Written so previously-stored content for the day stops
|
|
193
|
+
* being searchable instead of lingering as a stale transcript.
|
|
194
|
+
*/
|
|
195
|
+
function emptyDayBody(sourceId: string, date: string): string {
|
|
196
|
+
return (
|
|
197
|
+
`# ${sourceId} transcript — ${date}\n\n` +
|
|
198
|
+
"_No storable conversation content for this day (all segments were elided or dropped)._\n"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interface CleanedDay {
|
|
203
|
+
conversations: WearableConversation[];
|
|
204
|
+
segmentsKept: number;
|
|
205
|
+
segmentsDropped: number;
|
|
206
|
+
redactions: number;
|
|
207
|
+
correctionsApplied: number;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function cleanDay(
|
|
211
|
+
raw: WearableConversation[],
|
|
212
|
+
sourceId: string,
|
|
213
|
+
settings: WearableSourceSettings,
|
|
214
|
+
config: WearablesConfig,
|
|
215
|
+
userRedaction: RegExp[],
|
|
216
|
+
correctionRules: CompiledCorrectionRule[],
|
|
217
|
+
): CleanedDay {
|
|
218
|
+
const out: CleanedDay = {
|
|
219
|
+
conversations: [],
|
|
220
|
+
segmentsKept: 0,
|
|
221
|
+
segmentsDropped: 0,
|
|
222
|
+
redactions: 0,
|
|
223
|
+
correctionsApplied: 0,
|
|
224
|
+
};
|
|
225
|
+
for (const conversation of raw) {
|
|
226
|
+
let current = conversation;
|
|
227
|
+
if (config.offTheRecordEnabled) {
|
|
228
|
+
const otr = applyOffTheRecord(current);
|
|
229
|
+
current = otr.conversation;
|
|
230
|
+
out.segmentsDropped += otr.droppedSegments;
|
|
231
|
+
}
|
|
232
|
+
const cleaned = cleanConversation(current, settings.cleanup);
|
|
233
|
+
current = cleaned.conversation;
|
|
234
|
+
out.segmentsDropped += cleaned.droppedSegments;
|
|
235
|
+
|
|
236
|
+
const segments = current.segments.map((segment) => {
|
|
237
|
+
let text = segment.text;
|
|
238
|
+
if (config.redactionEnabled) {
|
|
239
|
+
const redacted = redactText(text, userRedaction);
|
|
240
|
+
text = redacted.text;
|
|
241
|
+
out.redactions += redacted.redactions;
|
|
242
|
+
}
|
|
243
|
+
const corrected = applyCorrections(text, correctionRules, sourceId);
|
|
244
|
+
out.correctionsApplied += corrected.applied;
|
|
245
|
+
return { ...segment, text: corrected.text };
|
|
246
|
+
});
|
|
247
|
+
current = { ...current, segments };
|
|
248
|
+
|
|
249
|
+
if (current.segments.length > 0) {
|
|
250
|
+
out.conversations.push(current);
|
|
251
|
+
out.segmentsKept += current.segments.length;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Sync one source. */
|
|
258
|
+
export async function syncWearableSource(
|
|
259
|
+
connector: WearableSourceConnector,
|
|
260
|
+
settings: WearableSourceSettings,
|
|
261
|
+
config: WearablesConfig,
|
|
262
|
+
options: WearableSyncOptions,
|
|
263
|
+
deps: WearableSyncDeps,
|
|
264
|
+
): Promise<WearableSyncSummary> {
|
|
265
|
+
const now = deps.now ? deps.now() : new Date();
|
|
266
|
+
const timezone = config.timezone ?? defaultTimezone();
|
|
267
|
+
const dates = resolveSyncDates(options, timezone, now);
|
|
268
|
+
|
|
269
|
+
const summary: WearableSyncSummary = {
|
|
270
|
+
source: connector.id,
|
|
271
|
+
days: dates,
|
|
272
|
+
conversations: 0,
|
|
273
|
+
segmentsKept: 0,
|
|
274
|
+
segmentsDropped: 0,
|
|
275
|
+
redactions: 0,
|
|
276
|
+
correctionsApplied: 0,
|
|
277
|
+
transcriptsWritten: [],
|
|
278
|
+
memoriesCreated: 0,
|
|
279
|
+
memoriesSkipped: 0,
|
|
280
|
+
nativeMemoriesImported: 0,
|
|
281
|
+
warnings: [],
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const registry = await loadSpeakerRegistry(deps.memoryDir);
|
|
285
|
+
const stateRules = await loadCorrectionsFile(deps.memoryDir);
|
|
286
|
+
const correctionRules = [
|
|
287
|
+
...compileCorrectionRules(config.corrections, "wearables.corrections"),
|
|
288
|
+
...compileCorrectionRules(stateRules, "state corrections"),
|
|
289
|
+
];
|
|
290
|
+
const userRedaction = compileRedactionPatterns(config.redactionPatterns);
|
|
291
|
+
|
|
292
|
+
let syncState = await loadSyncState(deps.memoryDir);
|
|
293
|
+
const previousState = syncState.sources[connector.id];
|
|
294
|
+
const dayHashes: Record<string, string> = {};
|
|
295
|
+
const memoryDayHashes: Record<string, string> = {};
|
|
296
|
+
const failedMemoryDays: string[] = [];
|
|
297
|
+
const importedNativeIds: string[] = [];
|
|
298
|
+
|
|
299
|
+
for (const date of dates) {
|
|
300
|
+
const fetched = await fetchAllConversationsForDate(
|
|
301
|
+
connector,
|
|
302
|
+
date,
|
|
303
|
+
timezone,
|
|
304
|
+
options.signal,
|
|
305
|
+
summary.warnings,
|
|
306
|
+
);
|
|
307
|
+
const cleaned = cleanDay(
|
|
308
|
+
fetched.conversations,
|
|
309
|
+
connector.id,
|
|
310
|
+
settings,
|
|
311
|
+
config,
|
|
312
|
+
userRedaction,
|
|
313
|
+
correctionRules,
|
|
314
|
+
);
|
|
315
|
+
summary.conversations += cleaned.conversations.length;
|
|
316
|
+
summary.segmentsKept += cleaned.segmentsKept;
|
|
317
|
+
summary.segmentsDropped += cleaned.segmentsDropped;
|
|
318
|
+
summary.redactions += cleaned.redactions;
|
|
319
|
+
summary.correctionsApplied += cleaned.correctionsApplied;
|
|
320
|
+
|
|
321
|
+
if (fetched.conversations.length === 0) {
|
|
322
|
+
// No provider data at all. A transient provider hiccup can
|
|
323
|
+
// legitimately produce an empty result, so an existing stored
|
|
324
|
+
// transcript is never auto-deleted here — surface it instead.
|
|
325
|
+
const existing = await deps.readDayContentHash(connector.id, date);
|
|
326
|
+
if (existing !== null) {
|
|
327
|
+
summary.warnings.push(
|
|
328
|
+
`${connector.id}: provider returned no conversations for ${date} but a stored transcript exists — leaving it in place; delete the day file manually if the recordings were intentionally removed upstream`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Provider data exists but cleanup/off-the-record elided all of it:
|
|
335
|
+
// replace any stored transcript with an explicit empty-day file so
|
|
336
|
+
// elided content stops being stored and searchable (Codex P2 on PR
|
|
337
|
+
// #1458).
|
|
338
|
+
const allElided = cleaned.conversations.length === 0;
|
|
339
|
+
let body = allElided
|
|
340
|
+
? emptyDayBody(connector.id, date)
|
|
341
|
+
: composeDayTranscriptBody(
|
|
342
|
+
connector.id,
|
|
343
|
+
date,
|
|
344
|
+
timezone,
|
|
345
|
+
cleaned.conversations,
|
|
346
|
+
registry,
|
|
347
|
+
);
|
|
348
|
+
if (fetched.partial && !allElided) {
|
|
349
|
+
body += PARTIAL_DAY_MARKER;
|
|
350
|
+
}
|
|
351
|
+
const bodyHash = hashTranscriptBody(body);
|
|
352
|
+
// The on-disk file is the authority for the skip decision — a hash
|
|
353
|
+
// remembered in sync state must never suppress recreating a day
|
|
354
|
+
// file that was deleted or lost (Cursor review on PR #1458). The
|
|
355
|
+
// state's dayHashes remain as bookkeeping only.
|
|
356
|
+
const existingHash = await deps.readDayContentHash(connector.id, date);
|
|
357
|
+
const changed = existingHash !== bodyHash;
|
|
358
|
+
// An all-elided day only writes a replacement over an existing
|
|
359
|
+
// file; it never creates an empty-day file from nothing.
|
|
360
|
+
const shouldWrite = changed && (!allElided || existingHash !== null);
|
|
361
|
+
|
|
362
|
+
if (shouldWrite) {
|
|
363
|
+
const meta = composeDayTranscriptMeta(
|
|
364
|
+
connector.id,
|
|
365
|
+
date,
|
|
366
|
+
timezone,
|
|
367
|
+
cleaned.conversations,
|
|
368
|
+
registry,
|
|
369
|
+
body,
|
|
370
|
+
now.toISOString(),
|
|
371
|
+
);
|
|
372
|
+
await deps.writeDayTranscript(
|
|
373
|
+
connector.id,
|
|
374
|
+
date,
|
|
375
|
+
serializeDayTranscript(meta, body),
|
|
376
|
+
);
|
|
377
|
+
summary.transcriptsWritten.push(date);
|
|
378
|
+
}
|
|
379
|
+
dayHashes[date] = bodyHash;
|
|
380
|
+
|
|
381
|
+
if (allElided) continue;
|
|
382
|
+
|
|
383
|
+
// The memory pass runs when the day changed, when forced, or when
|
|
384
|
+
// the last pass for this exact content didn't complete cleanly —
|
|
385
|
+
// a sync that stored the transcript but failed mid-memory-write
|
|
386
|
+
// self-heals on the next run instead of being frozen out by the
|
|
387
|
+
// unchanged-day skip (Cursor review on PR #1458).
|
|
388
|
+
const memoryPassComplete =
|
|
389
|
+
previousState?.memoryDayHashes?.[date] === bodyHash;
|
|
390
|
+
if (
|
|
391
|
+
settings.memoryMode !== "off" &&
|
|
392
|
+
(changed || options.forceMemories === true || !memoryPassComplete)
|
|
393
|
+
) {
|
|
394
|
+
if (!deps.memoryGen) {
|
|
395
|
+
summary.warnings.push(
|
|
396
|
+
`${connector.id}: memoryMode is '${settings.memoryMode}' but no extraction engine is available in this context — transcripts synced, memories skipped`,
|
|
397
|
+
);
|
|
398
|
+
} else {
|
|
399
|
+
// The whole memory pass (extraction, fact writes, digest) is
|
|
400
|
+
// warn-and-retry rather than abort: the transcript is already
|
|
401
|
+
// stored, and aborting here would leave any stale completion
|
|
402
|
+
// record from an earlier clean run in place to mask the
|
|
403
|
+
// failure (Kilo review on PR #1458 for the digest case; fact
|
|
404
|
+
// writes share the same failure class). A clean pass records
|
|
405
|
+
// completion; anything else clears it so the next sync
|
|
406
|
+
// re-runs the day.
|
|
407
|
+
let passClean = false;
|
|
408
|
+
try {
|
|
409
|
+
const generated = await generateWearableMemories(
|
|
410
|
+
connector.id,
|
|
411
|
+
date,
|
|
412
|
+
cleaned.conversations,
|
|
413
|
+
settings,
|
|
414
|
+
registry,
|
|
415
|
+
deps.memoryGen,
|
|
416
|
+
);
|
|
417
|
+
summary.memoriesCreated += generated.created;
|
|
418
|
+
summary.memoriesSkipped += generated.skipped;
|
|
419
|
+
summary.warnings.push(...generated.warnings);
|
|
420
|
+
passClean = generated.warnings.length === 0;
|
|
421
|
+
if (config.digestEnabled) {
|
|
422
|
+
const wrote = await writeDailyDigestMemory(
|
|
423
|
+
connector.id,
|
|
424
|
+
date,
|
|
425
|
+
cleaned.conversations,
|
|
426
|
+
settings,
|
|
427
|
+
registry,
|
|
428
|
+
deps.memoryGen.writer,
|
|
429
|
+
);
|
|
430
|
+
if (wrote) summary.memoriesCreated += 1;
|
|
431
|
+
}
|
|
432
|
+
} catch (err) {
|
|
433
|
+
passClean = false;
|
|
434
|
+
summary.warnings.push(
|
|
435
|
+
`${connector.id}: memory pass failed for ${date}: ${describeErrorForOperator(err)} — retries on the next sync`,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
if (passClean) {
|
|
439
|
+
memoryDayHashes[date] = bodyHash;
|
|
440
|
+
} else {
|
|
441
|
+
failedMemoryDays.push(date);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} else if (settings.memoryMode !== "off" && memoryPassComplete) {
|
|
445
|
+
// Carry the completion record forward for unchanged days.
|
|
446
|
+
memoryDayHashes[date] = bodyHash;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (
|
|
451
|
+
settings.importNativeMemories === "review" &&
|
|
452
|
+
typeof connector.fetchNativeMemories === "function"
|
|
453
|
+
) {
|
|
454
|
+
if (!deps.memoryGen) {
|
|
455
|
+
summary.warnings.push(
|
|
456
|
+
`${connector.id}: importNativeMemories is enabled but no memory writer is available in this context`,
|
|
457
|
+
);
|
|
458
|
+
} else {
|
|
459
|
+
const alreadyImported = new Set(
|
|
460
|
+
previousState?.importedNativeMemoryIds ?? [],
|
|
461
|
+
);
|
|
462
|
+
let cursor: string | null | undefined = undefined;
|
|
463
|
+
for (let page = 0; page < MAX_NATIVE_PAGES; page++) {
|
|
464
|
+
const result = await connector.fetchNativeMemories({
|
|
465
|
+
cursor,
|
|
466
|
+
signal: options.signal,
|
|
467
|
+
});
|
|
468
|
+
const imported = await importNativeMemories(
|
|
469
|
+
connector.id,
|
|
470
|
+
result.memories,
|
|
471
|
+
alreadyImported,
|
|
472
|
+
deps.memoryGen.writer,
|
|
473
|
+
);
|
|
474
|
+
summary.nativeMemoriesImported += imported.imported;
|
|
475
|
+
importedNativeIds.push(...imported.importedIds);
|
|
476
|
+
for (const id of imported.importedIds) alreadyImported.add(id);
|
|
477
|
+
if (!result.nextCursor) break;
|
|
478
|
+
cursor = result.nextCursor;
|
|
479
|
+
if (page === MAX_NATIVE_PAGES - 1) {
|
|
480
|
+
summary.warnings.push(
|
|
481
|
+
`${connector.id}: stopped native-memory import after ${MAX_NATIVE_PAGES} pages — remaining items import on the next sync`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (summary.transcriptsWritten.length > 0 && deps.afterTranscriptsWritten) {
|
|
489
|
+
try {
|
|
490
|
+
await deps.afterTranscriptsWritten();
|
|
491
|
+
} catch (err) {
|
|
492
|
+
summary.warnings.push(
|
|
493
|
+
`search reindex failed (transcripts are stored and will index on the next update): ${describeErrorForOperator(err)}`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Watermark advances only now — after transcript writes, memory
|
|
499
|
+
// writes, and native imports all succeeded.
|
|
500
|
+
syncState = updateSourceSyncState(syncState, connector.id, {
|
|
501
|
+
syncedAt: now.toISOString(),
|
|
502
|
+
days: dates,
|
|
503
|
+
dayHashes,
|
|
504
|
+
memoryDayHashes,
|
|
505
|
+
clearMemoryDays: failedMemoryDays,
|
|
506
|
+
importedNativeMemoryIds: importedNativeIds,
|
|
507
|
+
});
|
|
508
|
+
await saveSyncState(deps.memoryDir, syncState);
|
|
509
|
+
|
|
510
|
+
return summary;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function defaultTimezone(): string {
|
|
514
|
+
try {
|
|
515
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
516
|
+
} catch {
|
|
517
|
+
return "UTC";
|
|
518
|
+
}
|
|
519
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
applyOffTheRecord,
|
|
6
|
+
compileRedactionPatterns,
|
|
7
|
+
redactText,
|
|
8
|
+
REDACTION_PLACEHOLDER,
|
|
9
|
+
} from "./redaction.js";
|
|
10
|
+
import type { WearableConversation } from "./types.js";
|
|
11
|
+
|
|
12
|
+
test("redacts SSN-formatted numbers", () => {
|
|
13
|
+
const result = redactText("my social is 123-45-6789 okay", []);
|
|
14
|
+
assert.equal(result.text, `my social is ${REDACTION_PLACEHOLDER} okay`);
|
|
15
|
+
assert.equal(result.redactions, 1);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("redacts payment-card-like digit runs (spaced and contiguous)", () => {
|
|
19
|
+
assert.equal(
|
|
20
|
+
redactText("card 4111 1111 1111 1111 exp soon", []).text,
|
|
21
|
+
`card ${REDACTION_PLACEHOLDER} exp soon`,
|
|
22
|
+
);
|
|
23
|
+
assert.equal(
|
|
24
|
+
redactText("use 4111111111111111 today", []).text,
|
|
25
|
+
`use ${REDACTION_PLACEHOLDER} today`,
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("keeps short and ordinary numbers intact", () => {
|
|
30
|
+
const text = "call 555 0125 about the 2026 budget of $1,250";
|
|
31
|
+
const result = redactText(text, []);
|
|
32
|
+
assert.equal(result.text, text);
|
|
33
|
+
assert.equal(result.redactions, 0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("applies user patterns case-insensitively", () => {
|
|
37
|
+
const patterns = compileRedactionPatterns(["secret project \\w+"]);
|
|
38
|
+
const result = redactText("the Secret Project Falcon update", patterns);
|
|
39
|
+
assert.equal(result.text, `the ${REDACTION_PLACEHOLDER} update`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("compileRedactionPatterns rejects invalid regexes loudly", () => {
|
|
43
|
+
assert.throws(() => compileRedactionPatterns(["valid", "("]), /redactionPatterns\[1\]/);
|
|
44
|
+
assert.throws(() => compileRedactionPatterns([" "]), /non-empty/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function conversation(texts: string[]): WearableConversation {
|
|
48
|
+
return {
|
|
49
|
+
id: "c1",
|
|
50
|
+
source: "testsource",
|
|
51
|
+
startIso: "2026-06-10T10:00:00Z",
|
|
52
|
+
segments: texts.map((text, index) => ({
|
|
53
|
+
speakerKey: index % 2 === 0 ? "a" : "b",
|
|
54
|
+
text,
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test("off the record drops the span until back on the record", () => {
|
|
60
|
+
const result = applyOffTheRecord(
|
|
61
|
+
conversation([
|
|
62
|
+
"Let me say this off the record for a second.",
|
|
63
|
+
"The merger closes Friday.",
|
|
64
|
+
"Seriously, do not repeat that.",
|
|
65
|
+
"Okay, back on the record now.",
|
|
66
|
+
"Lunch was great.",
|
|
67
|
+
]),
|
|
68
|
+
);
|
|
69
|
+
const texts = result.conversation.segments.map((segment) => segment.text);
|
|
70
|
+
assert.deepEqual(texts, [
|
|
71
|
+
"[off the record — segment elided]",
|
|
72
|
+
"[back on the record]",
|
|
73
|
+
"Lunch was great.",
|
|
74
|
+
]);
|
|
75
|
+
assert.equal(result.droppedSegments, 2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("off the record without a closing marker drops through conversation end", () => {
|
|
79
|
+
const result = applyOffTheRecord(
|
|
80
|
+
conversation(["This is off the record.", "Private thing one.", "Private thing two."]),
|
|
81
|
+
);
|
|
82
|
+
assert.equal(result.conversation.segments.length, 1);
|
|
83
|
+
assert.equal(result.droppedSegments, 2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("conversations without the marker pass through untouched", () => {
|
|
87
|
+
const input = conversation(["Plain talk.", "More plain talk."]);
|
|
88
|
+
const result = applyOffTheRecord(input);
|
|
89
|
+
assert.deepEqual(
|
|
90
|
+
result.conversation.segments.map((segment) => segment.text),
|
|
91
|
+
["Plain talk.", "More plain talk."],
|
|
92
|
+
);
|
|
93
|
+
assert.equal(result.droppedSegments, 0);
|
|
94
|
+
});
|