ds4-context-engine 0.1.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.
@@ -0,0 +1,165 @@
1
+ # Memory and Persistent Pins
2
+
3
+ M9 separates durable, user-curated state from conversation history while keeping Pi JSONL canonical.
4
+
5
+ ## Authority and scope
6
+
7
+ Pins are explicit user-confirmed context with maximum planner priority:
8
+
9
+ - `session`: available throughout the current Pi session tree;
10
+ - `branch`: available only when its creation leaf is on the active branch;
11
+ - `project`: available to sessions using the same canonical project path, but only when Pi reports the project trusted.
12
+
13
+ Memory is quoted historical data rather than policy:
14
+
15
+ - `session`: available only in the source Pi session;
16
+ - `project`: shared across sessions for the same trusted canonical project path.
17
+
18
+ Pins remain subordinate to system/developer instructions. Memory tells the model to validate a claim against current evidence. Neither is created automatically in M9.
19
+
20
+ ## Commands
21
+
22
+ ```text
23
+ /context pins
24
+ /context pin [--scope session|branch|project] [--classification LEVEL] [--source ENTRY] [--file PATH] <content>
25
+ /context pin --scope session --supersedes PIN_ID [--classification LEVEL] <replacement>
26
+ /context unpin PIN_ID [reason]
27
+
28
+ /context memory
29
+ /context memory add [--scope session|project] [--classification LEVEL] [--key KEY] [--source ID,ID] <claim>
30
+ /context memory supersede MEMORY_ID [--classification LEVEL] [--source ID,ID] <new claim>
31
+ /context memory invalidate MEMORY_ID [reason]
32
+ /context memory expire MEMORY_ID [reason]
33
+ ```
34
+
35
+ Arguments support single/double quotes and backslash escaping. `--` ends option parsing. Source entry IDs must be on Pi's active branch. Project scope requires Pi project trust.
36
+
37
+ Mutations are manual-first. Repeating the same normalized pin/claim returns its existing ID without appending another entry.
38
+
39
+ ## Canonical append-only mutations
40
+
41
+ Every accepted operation is appended through `pi.appendEntry()` as a Pi `CustomEntry`:
42
+
43
+ ```text
44
+ ds4-context-pin-v1
45
+ ds4-context-memory-v1
46
+ ```
47
+
48
+ Custom entries do not participate directly in Pi's LLM context. Their payload is an immutable versioned mutation:
49
+
50
+ ```text
51
+ add
52
+ supersede previous ID with a new immutable record
53
+ status -> deleted / invalid / expired
54
+ ```
55
+
56
+ No row is silently overwritten. SQLite mutation and materialized tables are disposable projections. On startup or `/context rebuild-index`, DS4:
57
+
58
+ 1. indexes the custom entries with their canonical scoped entry keys;
59
+ 2. replaces the current session's mutation projection;
60
+ 3. replays all known mutations in timestamp + canonical entry order;
61
+ 4. rebuilds memory, pin, source, lifecycle, and FTS rows transactionally.
62
+
63
+ Deleting `context.db` and reopening the canonical source session reconstructs its state. Project items from other sessions reappear when those canonical sessions are replayed.
64
+
65
+ ## Supersession and contradiction handling
66
+
67
+ A memory may have an explicit normalized key:
68
+
69
+ ```text
70
+ /context memory add --key package-export-mode \
71
+ "Package export mode defaults to PerEndpoint."
72
+ ```
73
+
74
+ For claims shaped like `subject defaults to value`, `subject is value`, or `subject = value`, DS4 derives a conservative key automatically. An active item with the same scope/key and a different claim is a conflict. Opposite-polarity claims such as `Feature is enabled` and `Feature is disabled` are also detected when their normalized bases match.
75
+
76
+ A conflicting `add` is rejected with the IDs involved. The user must issue explicit supersession:
77
+
78
+ ```text
79
+ /context memory supersede MEMORY_ID \
80
+ "Package export mode defaults to SingleFile."
81
+ ```
82
+
83
+ The old item remains stored as `superseded` and points to the new active item. During replay, concurrent active records with the same key are both preserved; the deterministic later record is marked `invalid` with a conflict reason rather than replacing the earlier one.
84
+
85
+ Pins use the same immutable replacement pattern through `--supersedes`. `/context unpin` records a soft `deleted` lifecycle mutation.
86
+
87
+ ## Context selection
88
+
89
+ The managed planner inserts selected synthetic messages immediately before the current request in this order:
90
+
91
+ ```text
92
+ persistent pins priority 950, mandatory within maxPinnedTokens
93
+ memory priority 90, maxMemoryTokens
94
+ historical retrieval priority 85
95
+ project snippets priority 80
96
+ current request mandatory
97
+ ```
98
+
99
+ All applicable pins are considered in deterministic creation order. Branch pins are hard-filtered against `SessionManager.getBranch()`.
100
+
101
+ Memory ranking uses current request identifiers, file/symbol/keyword terms, optional keys, scope authority, and recency. If any memories match, unrelated memory is excluded. When nothing matches, at most three recent active items provide conservative continuity. `memory.maxResults` and `context.maxMemoryTokens` bound the final set.
102
+
103
+ A pin budget overflow causes planner fallback rather than silently splitting mandatory context. Commands cap individual pin/claim characters, while final system/tools/messages validation still enforces the active model hard limit.
104
+
105
+ ## Prompt-injection boundary
106
+
107
+ Pins are rendered as user-confirmed content:
108
+
109
+ ```text
110
+ [DS4 PINNED CONTEXT — USER-CONFIRMED]
111
+ ...
112
+ Pinned content JSON: "..."
113
+ [END DS4 PINNED CONTEXT]
114
+ ```
115
+
116
+ Memory is rendered as quoted data:
117
+
118
+ ```text
119
+ [DS4 DURABLE MEMORY — QUOTED DATA]
120
+ ...
121
+ Claim JSON: "..."
122
+ [END DS4 DURABLE MEMORY]
123
+ ```
124
+
125
+ User text is JSON-quoted. Context Manifests contain IDs, scope, classification, key, source session/entry IDs, score, token estimate, and selection reason—but never pin content or memory claim text.
126
+
127
+ M10 stores an optional classification in the canonical mutation. Before a remote call, a prohibited pin/memory is omitted as a whole and recorded only by ID/classification/reason. Explicit classification cannot be downgraded by markers inside its content. See [`PRIVACY.md`](PRIVACY.md).
128
+
129
+ ## SQLite schema v9
130
+
131
+ Schema v9 extends materialized `memory_items` and `pins` with:
132
+
133
+ - normalized key, origin session, branch leaf;
134
+ - update/status reason, optional classification in `metadata_json`, and immutable supersession links;
135
+ - indexes for scope/lifecycle/branch selection.
136
+
137
+ `memory_mutations` and `pin_mutations` reference canonical indexed Pi custom entries. `memory_sources` retains exact scoped entry provenance. `memory_fts` is rebuilt transactionally.
138
+
139
+ Migration preserves legacy materialized rows for inspection. Because they have no canonical mutation entry, a later full replay may discard them; new operations are always event-sourced.
140
+
141
+ ## Diagnostics
142
+
143
+ ```text
144
+ /context status
145
+ /context tokens
146
+ /context manifest
147
+ /context included
148
+ /context excluded
149
+ /context pins
150
+ /context memory
151
+ /context health
152
+ /context rebuild-index
153
+ ```
154
+
155
+ Structured logs contain mutation/item IDs, scopes, counts, lifecycle, and warning counts—not pin or claim text.
156
+
157
+ ## Performance
158
+
159
+ `tests/benchmarks/memory-selection.bench.ts` ranks 1,000 active session/project memories. On the development host:
160
+
161
+ ```text
162
+ mean 5.85 ms, p99 7.22 ms, max 8.49 ms
163
+ ```
164
+
165
+ This is below the initial 50 ms typical context-planning target; it is not a portable guarantee.
@@ -0,0 +1,131 @@
1
+ # Advanced Model Awareness
2
+
3
+ M11 resolves an independent deterministic planning profile for every exact `provider/model`. Pi's model descriptor remains the default source for context window, output ceiling, reasoning, and image support; explicit DS4 overrides can repair provider metadata or tune category limits without changing canonical session state.
4
+
5
+ ## Profile resolution
6
+
7
+ Overrides merge from least to most specific:
8
+
9
+ ```text
10
+ *
11
+ provider/*
12
+ provider/model-id
13
+ ```
14
+
15
+ Supported fields are:
16
+
17
+ ```text
18
+ contextWindow
19
+ maxOutputTokens
20
+ safetyMarginTokens
21
+ recentTailTokens
22
+ maxRetrievedHistoryTokens
23
+ maxProjectTokens
24
+ ```
25
+
26
+ Unknown fields, malformed keys, non-integer token limits, impossible output/window combinations, and unsafe calibration settings reject that configuration source. Global and trusted-project override maps merge by key. Project configuration remains subject to Pi's project trust decision.
27
+
28
+ Example:
29
+
30
+ ```json
31
+ {
32
+ "modelAwareness": {
33
+ "enabled": true,
34
+ "calibrationWindow": 24,
35
+ "minimumCalibrationSamples": 3,
36
+ "calibrationRatioLowerBound": 0.5,
37
+ "calibrationRatioUpperBound": 2.0,
38
+ "overrides": {
39
+ "*": { "safetyMarginTokens": 2048 },
40
+ "ollama/*": { "recentTailTokens": 16000 },
41
+ "openrouter/vendor/model": {
42
+ "contextWindow": 200000,
43
+ "maxRetrievedHistoryTokens": 12000
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ Setting `modelAwareness.enabled` to `false` disables calibration, profile overrides, and adaptive history/project retrieval. The pre-M11 model-adaptive recent-tail safety ceiling remains active.
51
+
52
+ ## Adaptive category budgets
53
+
54
+ With no explicit override, category ceilings derive from the resolved context window and remain bounded by the corresponding `context.*` maximum:
55
+
56
+ | Context window | Recent tail | Historical retrieval | Project retrieval |
57
+ |---|---:|---:|---:|
58
+ | up to 40k | 12k | 4k | 4k |
59
+ | up to 128k | 24k | 8k | 12k |
60
+ | up to 256k | 32k | 16k | 20k |
61
+ | larger | 64k | 32k | 32k |
62
+
63
+ The default `context.recentTailTokens` is 64k so these automatic tiers are effective by default. Category limits are independent: a small model does not spend its entire input on retrieval, while a large model can retain more verbatim work and source evidence.
64
+
65
+ ## Model-specific token calibration
66
+
67
+ Each successful assistant response is correlated with one pending Context Manifest. Provider input is defined exactly as Pi defines request input usage:
68
+
69
+ ```text
70
+ actualInputTokens = input + cacheRead + cacheWrite
71
+ ratio = actualInputTokens / raw chars-v1 estimate
72
+ ```
73
+
74
+ Calibration is isolated by exact provider and model ID. The latest configured window is validated and processed deterministically:
75
+
76
+ 1. reject invalid or zero samples;
77
+ 2. reject ratios outside the configured hard bounds;
78
+ 3. compute the bounded median and median absolute deviation (MAD);
79
+ 4. reject values outside the larger of 0.05, 10% of the median, or three scaled MADs;
80
+ 5. require `minimumCalibrationSamples` accepted values;
81
+ 6. use the accepted median as the next-call multiplier.
82
+
83
+ Until enough samples exist, the multiplier remains `1.0`. The default window is 24 samples, minimum is 3, and hard ratio bounds are 0.5–2.0. Outliers remain counted in metadata but do not influence the applied ratio.
84
+
85
+ Provider-token capacities are computed first from context window, output reserve, safety margin, and policy ratios. Planner limits are then converted into local-estimator units by dividing by the accepted multiplier. Raw manifest estimates remain uncalibrated so future samples do not feed a corrected estimate back into itself. Adaptive tail/history/project budgets use the same conversion.
86
+
87
+ ## Provider cache metrics
88
+
89
+ Schema v10 stores separate per-call values for:
90
+
91
+ ```text
92
+ input_tokens
93
+ cache_read_tokens
94
+ cache_write_tokens
95
+ ```
96
+
97
+ The manifest records the finalized call's three values, total provider input, and read/write shares. The active model profile reports aggregate totals and shares over its calibration window. Error, aborted, missing, zero-usage, duplicate-response, or uncorrelated messages create no sample.
98
+
99
+ Cache metrics are observational. Stable deterministic ordering and unchanged system/tool prefixes make cache reuse possible, but the provider decides whether a prefix is reusable. M12's separately configured native continuation may attach a real provider response handle only after exact hashed-prefix validation; it never fabricates a handle or treats cache/continuation state as canonical. See [`NATIVE_CONTINUATION.md`](NATIVE_CONTINUATION.md).
100
+
101
+ ## Model and provider switches
102
+
103
+ `model_select` records source (`set`, `cycle`, or `restore`), previous profile, whether the exact profile was seen before, and cache disposition. A change to a different provider/model is treated as a cold cache boundary and invalidates volatile native-continuation state. Switching back reuses that exact model's calibration history and adaptive policy, not another model's ratio.
104
+
105
+ Every subsequent `context` call rebuilds provider-facing context from canonical Pi JSONL and current derived indexes. It reruns privacy classification for the new destination, so a local-to-remote switch can remove `local-only` content while a later switch back to an allowed local model can recover it from canonical state. No switch mutates or truncates the session, memory/pin mutations, project source, artifact bytes, or summary provenance.
106
+
107
+ ## Manifest and diagnostics
108
+
109
+ The metadata-only Context Manifest includes:
110
+
111
+ - profile key, effective window/output/safety margin, and matched override keys;
112
+ - calibration window, bounds, accepted/rejected/outlier counts, median, and applied ratio;
113
+ - adaptive nominal and estimator-adjusted category limits;
114
+ - cache totals/shares over the model window;
115
+ - switch source, prior profile, reuse flag, and cache disposition;
116
+ - finalized per-call uncached input, cache-read, cache-write, and total input usage.
117
+
118
+ It never stores prompt text, provider payloads, cache keys, headers, classified spans, or credentials.
119
+
120
+ Use:
121
+
122
+ ```text
123
+ /context model
124
+ /context tokens
125
+ /context manifest
126
+ /context status
127
+ ```
128
+
129
+ ## Performance and tests
130
+
131
+ `tests/benchmarks/model-awareness.bench.ts` measures a bounded 200-sample calibration analysis and repeated 32k/128k/200k profile resolution. Unit and golden tests cover deterministic tiers, override precedence, robust outlier rejection, cache accounting, and calibrated budgets. Integration tests switch local/remote providers and 32k/128k/200k models while checking profile isolation, privacy re-enforcement, canonical JSONL preservation, SQLite cache metrics, and profile reuse.
@@ -0,0 +1,127 @@
1
+ # Optional Native Continuation
2
+
3
+ M12 adds an opt-in OpenAI Responses optimization without making provider state canonical.
4
+
5
+ ## Decision
6
+
7
+ Pi 0.84.3 exposes two relevant integration surfaces:
8
+
9
+ - `before_provider_request` can replace the serialized payload, but it cannot transparently retry the same model call after a provider rejects stale state;
10
+ - `registerProvider(..., { streamSimple })` can delegate to Pi's built-in serializer/transport while observing the terminal stream and retrying before any partial assistant event is exposed.
11
+
12
+ DS4 therefore registers a narrow provider wrapper only for explicitly configured providers. The wrapper delegates to Pi's built-in `openai-responses` implementation. It does not implement authentication, HTTP, SSE parsing, tool serialization, usage accounting, or model discovery itself.
13
+
14
+ OpenAI conversation IDs were evaluated but are not enabled in M12. `previous_response_id` is the smaller state primitive, requires no second canonical conversation tree, and can always fall back to the full managed replay. Pi's `openai-codex-responses` transport already has its own connection-scoped cached continuation and is not replaced by this wrapper.
15
+
16
+ ## Explicit provider-storage consent
17
+
18
+ Native continuation is disabled by default. Eligible requests require both:
19
+
20
+ ```json
21
+ {
22
+ "nativeContinuation": {
23
+ "enabled": true,
24
+ "allowProviderStorage": true,
25
+ "profiles": ["openai/*"],
26
+ "maxStateAgeMs": 1800000,
27
+ "retryManagedReplay": true
28
+ }
29
+ }
30
+ ```
31
+
32
+ `allowProviderStorage` acknowledges that DS4 changes eligible OpenAI Responses payloads from Pi's default `store: false` to `store: true`. Provider-side retention and deletion are governed by the selected provider. `maxStateAgeMs` limits only reuse of the volatile local handle; it does not delete provider-side data.
33
+
34
+ Profiles must be exact `provider/model` values or explicit `provider/*` wildcards. A global wildcard is rejected. Model IDs may contain `/`, for example `proxy/vendor/model`.
35
+
36
+ A trusted project configuration may enable this feature. Configuration from an untrusted project remains ignored by the existing trust gate.
37
+
38
+ ## Request algorithm
39
+
40
+ For an eligible main-agent request, after all Pi `before_provider_request` handlers have produced the final privacy-sanitized payload, DS4:
41
+
42
+ 1. normalizes the full request to `store: true`;
43
+ 2. hashes every full `input` item independently with deterministic SHA-256;
44
+ 3. hashes all non-input request options, including model, tools, output settings, and cache settings;
45
+ 4. compares the current input prefix with:
46
+
47
+ ```text
48
+ previous full request input
49
+ + previous serialized response output items
50
+ ```
51
+
52
+ 5. only on an exact hash match sends:
53
+
54
+ ```json
55
+ {
56
+ "previous_response_id": "<volatile handle>",
57
+ "input": ["only new suffix items"],
58
+ "store": true
59
+ }
60
+ ```
61
+
62
+ 6. otherwise sends the complete DS4-managed payload and establishes a new chain from the successful response.
63
+
64
+ The manager retains hashes plus the minimum volatile response handle. It does not retain prompt text, tool payloads, or response output text. The provider handle is never logged or copied into a Context Manifest. Pi may persist the provider's normal `AssistantMessage.responseId` in canonical JSONL; DS4 adds no custom canonical entry or SQLite continuation table.
65
+
66
+ ## Transparent managed-replay retry
67
+
68
+ If a continuation request is rejected before streaming starts and the provider error identifies invalid, expired, unknown, or missing previous-response state, the wrapper:
69
+
70
+ 1. suppresses that unstarted error event;
71
+ 2. clears the volatile handle;
72
+ 3. rebuilds the request through Pi's normal serializer and all payload hooks;
73
+ 4. retries once with the complete managed replay and no `previous_response_id`.
74
+
75
+ Unrelated failures such as authentication or rate limiting are not retried by this layer. Errors after a stream has started are never replayed because doing so could duplicate visible output or tool effects. Pi's ordinary retry policy remains available after DS4 returns a terminal failure.
76
+
77
+ ## Conservative invalidation
78
+
79
+ State is discarded on:
80
+
81
+ - provider or model switch;
82
+ - session branch/tree navigation;
83
+ - successful compaction;
84
+ - session shutdown/reload or degraded runtime;
85
+ - local state age above `maxStateAgeMs`;
86
+ - changed model/tools/output/cache request options;
87
+ - any managed-context prefix mismatch, including changed summaries, privacy filtering, project evidence, pins, memory, or active tools;
88
+ - empty deltas;
89
+ - missing provider response IDs or non-reconstructable response items;
90
+ - provider request/stream failure.
91
+
92
+ Nested DS4 summary calls use a fresh Pi `sessionId` and are never opted into provider storage or continuation. Observer-mode calls also remain unchanged.
93
+
94
+ ## Privacy and canonical state
95
+
96
+ The continuation transform runs after Pi's payload callback chain, so it only removes an already checked prefix and adds state metadata. It never reintroduces classified content. If enabled privacy enforcement replaces a failed payload with `{}`, continuation is skipped and the provider call fails closed as before.
97
+
98
+ The full Pi JSONL history, DS4 summaries, memory/pins, and Context Manifests remain sufficient for managed replay. Deleting or expiring provider state affects optimization only.
99
+
100
+ ## Diagnostics
101
+
102
+ Use:
103
+
104
+ ```text
105
+ /context continuation
106
+ /context manifest
107
+ /context status
108
+ /context health
109
+ ```
110
+
111
+ Diagnostics expose only:
112
+
113
+ - enabled/consent/profile/wrapper state;
114
+ - full versus continuation request counts;
115
+ - full/sent/omitted input-item counts;
116
+ - state age, generic fallback/invalidation reason, and retry outcome;
117
+ - warning counts.
118
+
119
+ They never expose provider response IDs or provider payload content.
120
+
121
+ ## Tests and performance
122
+
123
+ Unit tests cover exact/wildcard eligibility, consent validation, cold establishment, exact suffix derivation, option/prefix/age invalidation, nested-call exclusion, and response-ID non-disclosure. Stream tests cover pre-stream stale-handle replay and refusal to retry unrelated failures. Integration coverage verifies provider registration, metadata-only manifests/SQLite, and branch invalidation.
124
+
125
+ `tests/benchmarks/native-continuation.bench.ts` hashes and verifies a 1,000-item managed prefix. On the development host it measured about 2.88 ms mean and 5.39 ms p99, below the existing 50 ms context-operation target; this is not a portable guarantee.
126
+
127
+ A real Pi 0.84.3 isolated RPC E2E used a local OpenAI Responses-compatible HTTP server for three turns. It observed full → one-item delta → rejected stale delta → automatic full replay, with `store: true`, successful retry diagnostics, canonical JSONL preservation, metadata-only manifests, SQLite integrity/FK checks, and zero `extension_error` events.
@@ -0,0 +1,92 @@
1
+ # Portable Core
2
+
3
+ M13 extracts the runtime-neutral implementation into the independently buildable `ds4-context-core` workspace package. The root `ds4-context-engine` package remains the Pi integration.
4
+
5
+ ## Dependency rule
6
+
7
+ ```text
8
+ agent runtime
9
+ ↓
10
+ runtime adapter
11
+ ↓
12
+ ds4-context-core
13
+ ```
14
+
15
+ Core never imports an adapter or runtime SDK. The Pi adapter imports core through its public ESM exports.
16
+
17
+ The core may use Node.js standard-library facilities such as `node:sqlite`, filesystem APIs and cryptographic hashing. Portable means independent of Pi and reusable by another Node-based agent runtime; it does not mean browser-compatible.
18
+
19
+ ## Package contents
20
+
21
+ `packages/core/src` owns:
22
+
23
+ - canonical message and model projections;
24
+ - token estimation, calibration and context budgets;
25
+ - deterministic planning and atomic group validation;
26
+ - manifest and provenance models;
27
+ - summary contracts, validation and graph records;
28
+ - historical and project retrieval;
29
+ - project indexing and artifact policy;
30
+ - memory/pin materialization;
31
+ - privacy classification and provider policy;
32
+ - native-continuation eligibility and hash state;
33
+ - rebuildable SQLite repositories;
34
+ - stable serialization, hashing and logging.
35
+
36
+ The root adapter owns all Pi-specific behavior:
37
+
38
+ - Pi message, model and session conversion;
39
+ - JSONL branch and custom-entry projection;
40
+ - extension lifecycle hooks and commands;
41
+ - summary model completion through Pi's registry;
42
+ - OpenAI Responses transport wrapping through Pi AI;
43
+ - fail-open integration orchestration and privacy fail-closed enforcement.
44
+
45
+ ## Adapter responsibilities
46
+
47
+ A future runtime adapter must:
48
+
49
+ 1. preserve its runtime's canonical history and expose stable source identifiers;
50
+ 2. convert native messages to DS4 canonical messages without losing tool-call/result atomicity;
51
+ 3. describe provider/model limits using the core model projection;
52
+ 4. supply the current branch, request, system prompt, tools and trusted project path;
53
+ 5. apply core plans without persisting provider-facing synthetic context as canonical history;
54
+ 6. append memory/pin mutations to canonical runtime history before materializing derived state;
55
+ 7. invoke model completion at the adapter boundary for generated summaries;
56
+ 8. enforce privacy immediately before provider transport;
57
+ 9. discard or rebuild SQLite and artifact projections safely;
58
+ 10. fall back to native runtime behavior when operational integration fails.
59
+
60
+ ## Build and exports
61
+
62
+ ```bash
63
+ npm run build:core
64
+ npm run typecheck
65
+ npm test
66
+ npm run pack:check
67
+ npm pack --dry-run --workspace ds4-context-core
68
+ ```
69
+
70
+ TypeScript sources compile to `packages/core/dist` as ESM JavaScript, source maps and declaration files. The npm package exports a top-level API and fine-grained subpaths such as:
71
+
72
+ ```ts
73
+ import { calculateContextBudget } from "ds4-context-core";
74
+ import { planManagedContext } from "ds4-context-core/planner/context-planner";
75
+ ```
76
+
77
+ The Pi package declares an exact same-release dependency on `ds4-context-core`. Release order is therefore core first, adapter second. `npm run pack:check` verifies both tarball inventories, installs them together in a clean temporary consumer, probes core ESM exports and starts the packaged adapter with isolated Pi RPC state. See [Releasing DS4](RELEASING.md) for the publication checklist.
78
+
79
+ ## Enforcement
80
+
81
+ `tests/unit/portable-core-boundary.test.ts` recursively rejects Pi SDK and adapter imports from core source, then imports the compiled package and exercises portable model/budget policy. Existing integration tests consume core through package exports, so the Pi adapter is tested across the actual package boundary.
82
+
83
+ ## State guarantees
84
+
85
+ Extraction does not change DS4 state semantics:
86
+
87
+ - Pi JSONL remains canonical for Pi sessions;
88
+ - SQLite remains disposable and rebuildable;
89
+ - compaction remains non-destructive and strictly validated;
90
+ - provider continuation handles remain volatile;
91
+ - planner, retrieval, compaction and persistence failures still fail open at the adapter boundary;
92
+ - enabled privacy enforcement still fails closed before remote transport.
@@ -0,0 +1,157 @@
1
+ # Privacy and Remote Provider Policy
2
+
3
+ M10 adds an opt-in, provider-aware privacy boundary around the existing managed context. Pi JSONL remains canonical and may retain restricted local content; the policy controls only provider-facing copies, summary-generation input, artifact excerpts, and the final serialized provider payload.
4
+
5
+ Privacy is disabled by default for backward compatibility.
6
+
7
+ ## Classifications
8
+
9
+ DS4 recognizes four classifications, ordered from least to most restrictive:
10
+
11
+ ```text
12
+ normal < internal < sensitive < local-only
13
+ ```
14
+
15
+ `local-only` is a hard ceiling: configuration validation rejects every remote allow rule containing it. A marker cannot downgrade a stricter default or explicit classification.
16
+
17
+ Content can be marked inline:
18
+
19
+ ```text
20
+ \[ds4:internal]team-only context\[/ds4:internal]
21
+ \[ds4:sensitive]restricted value\[/ds4:sensitive]
22
+ \[ds4:local-only]must remain on this machine\[/ds4:local-only]
23
+ ```
24
+
25
+ Remove the example backslashes to activate the markers. A leading marker without a closing tag classifies the remainder of that text. Unbalanced markers split across content blocks conservatively classify the complete message. Markers are control metadata: allowed local/remote copies contain the enclosed text without the tags; disallowed copies contain only a classification placeholder. Prefix a marker's opening bracket with `\` when the syntax itself must remain literal source/documentation.
26
+
27
+ Persistent pin and memory commands also accept an explicit classification:
28
+
29
+ ```text
30
+ /context pin --scope project --classification local-only -- "Private deployment rule"
31
+ /context memory add --scope project --classification sensitive --key release-channel -- "Release channel defaults to private."
32
+ /context memory supersede MEMORY_ID --classification internal -- "Replacement claim"
33
+ ```
34
+
35
+ The classification is part of the canonical Pi custom-entry mutation and survives SQLite deletion, session resume, branch changes, and compaction. Existing unclassified mutations use `privacy.defaultClassification` at selection time.
36
+
37
+ Automatic sensitivity classification remains disabled. M10 uses explicit markers, explicit pin/memory metadata, and the configured default.
38
+
39
+ ## Provider destination and allow rules
40
+
41
+ All providers are treated as remote unless their exact, case-insensitive provider ID appears in `localProviders`.
42
+
43
+ ```json
44
+ {
45
+ "privacy": {
46
+ "enabled": true,
47
+ "defaultClassification": "normal",
48
+ "localProviders": ["ollama", "llama-cpp", "lmstudio"],
49
+ "remoteDefaultAllowed": ["normal", "internal"],
50
+ "remoteProviders": {
51
+ "openrouter": ["normal"],
52
+ "anthropic": ["normal", "internal"],
53
+ "private-gateway": ["normal", "internal", "sensitive"]
54
+ },
55
+ "redactSecrets": true
56
+ }
57
+ }
58
+ ```
59
+
60
+ Rules are exact allow lists, not maximum-enum shorthand. `remoteDefaultAllowed` applies to unknown remote providers. Provider-specific rules override it. A provider cannot appear in both `localProviders` and `remoteProviders`. Only mark a provider local when its transport is guaranteed to remain local; a trusted project configuration has the same execution authority as other trusted Pi project resources.
61
+
62
+ Defaults when privacy is enabled:
63
+
64
+ ```text
65
+ known configured local provider -> normal, internal, sensitive, local-only
66
+ remote provider override -> exact configured allow list
67
+ other provider -> normal, internal
68
+ ```
69
+
70
+ The shipped `faux` test provider is in the default local list. Real unknown/custom provider IDs are remote by default.
71
+
72
+ ## Enforcement pipeline
73
+
74
+ ### Context hook
75
+
76
+ Before artifact condensation, retrieval, or planning, DS4 sanitizes every native `AgentMessage` content field and records a classification without storing its text in diagnostics. Tool call arguments, text/thinking blocks, tool results, and image/binary content participate; protocol identifiers such as role, type, model, tool name, and call ID remain intact.
77
+
78
+ DS4 separately sanitizes the effective system prompt and active tool descriptions/schema text for token accounting and manifest hashing. The final provider hook applies the same policy to their actual serialized forms.
79
+
80
+ Historical retrieval, project snippets, pin, and memory supplements are checked independently. A supplement containing a prohibited block is omitted as a whole and receives a metadata-only `excluded due to privacy policy` record. Native turns remain structurally valid: prohibited spans become placeholders so current-user and complete tool-call/result atomic groups survive.
81
+
82
+ Planner exceptions return the already-sanitized native array. If privacy preparation itself fails, every message content field is replaced with a fail-closed placeholder while protocol identity is preserved. Privacy enforcement therefore does not use the ordinary fail-open rule.
83
+
84
+ ### Compaction and summary graph
85
+
86
+ Conversation text, custom instructions, and file lists are sanitized before summary generation. Validation runs against the sanitized source. Generated segment/aggregate nodes inherit the highest source classification and persist it as a DS4 marker around the summary. A local model may summarize local-only source, but switching to a remote provider strips that summary before it can be serialized.
87
+
88
+ Pi's fallback compactor is still covered by the final provider-payload hook.
89
+
90
+ ### Artifact store and search
91
+
92
+ The local content-addressed object may retain exact restricted bytes because Pi JSONL is canonical and object hashes require exact recovery. Artifact references persist the derived classification in `metadata_json`. Context selection hides prohibited tool results before offload/reference injection. `context_artifact_search` applies the stored artifact classification to every returned excerpt; a remote request receives no matches/content for a prohibited artifact.
93
+
94
+ ### Final provider payload
95
+
96
+ `before_provider_request` runs after provider-specific serialization. DS4 recursively checks known provider content containers (`system`, `messages`, `input`, `contents`, `context`, tool descriptions/arguments, and related text fields), strips classification markers, removes prohibited blocks, and redacts credential-like values. Structural provider fields remain unchanged.
97
+
98
+ The handler catches its own failures because Pi 0.84.3 reports extension-hook exceptions and would otherwise continue with the unchanged payload. On an unexpected sanitizer failure DS4 returns an empty object, intentionally causing the provider request to fail rather than leaking content.
99
+
100
+ Extension handlers execute in load order. A later extension can replace the payload after DS4. For the strongest boundary, load DS4 after every extension that mutates context or provider payloads. DS4 cannot police network traffic created directly by another extension outside Pi's provider pipeline.
101
+
102
+ ### Optional native continuation
103
+
104
+ When M12 native continuation is explicitly enabled, the registered OpenAI Responses wrapper invokes Pi's complete payload callback chain first. It then hashes and validates the final sanitized full prefix, removes only an exact already-sent prefix, and adds `store: true` plus a volatile `previous_response_id`. It never adds content or bypasses the final privacy sanitizer. A fail-closed `{}` payload is ineligible and remains `{}`.
105
+
106
+ `allowProviderStorage: true` is mandatory because provider persistence changes the privacy/retention posture. Profiles are explicit and cannot use a global wildcard. Response IDs, payload text, and item hashes are absent from manifests and logs. See [`NATIVE_CONTINUATION.md`](NATIVE_CONTINUATION.md).
107
+
108
+ ## Secret redaction
109
+
110
+ For remote destinations, `redactSecrets` recognizes common private-key blocks, Bearer tokens, OpenAI/Anthropic-style keys, GitHub tokens, AWS access IDs, and common key/token/password assignments. It is defense in depth, not an automatic data-classification oracle. Local providers retain allowed exact content.
111
+
112
+ Redaction is performed on provider-facing copies only. Canonical JSONL, local artifact objects, and trusted local SQLite indexes may retain source bytes.
113
+
114
+ ## Manifest and logging contract
115
+
116
+ Context Manifest items may contain:
117
+
118
+ - classification;
119
+ - source ID/kind and inclusion/exclusion reason;
120
+ - counts by selected classification;
121
+ - provider ID and local/remote destination;
122
+ - allowed classification names;
123
+ - blocked/excluded/redacted counts;
124
+ - final provider-check/redaction counts;
125
+ - enforcement stage.
126
+
127
+ They never contain classified text, memory claims, pin content, project/retrieval excerpts, provider payloads, or secret values. A provider check updates the pending manifest in place after serialization.
128
+
129
+ Structured privacy logs contain provider/destination and numeric counters only. On a fail-closed exception DS4 records only the exception type, never its message or payload.
130
+
131
+ Inspect locally with:
132
+
133
+ ```text
134
+ /context privacy
135
+ /context manifest
136
+ /context included
137
+ /context excluded
138
+ /context health
139
+ ```
140
+
141
+ ## Tests and performance
142
+
143
+ Coverage includes:
144
+
145
+ - span and prefix classification, no-downgrade, split-marker fail-closed behavior;
146
+ - local/remote and provider-specific rules;
147
+ - current messages, system prompt, tools, history, project, pins, memory, summaries, and artifacts;
148
+ - secret redaction and metadata-only manifests/logs;
149
+ - provider switch, planner/preparation failure, final provider-payload recheck, and real Pi faux-provider E2E.
150
+
151
+ `tests/benchmarks/privacy-policy.bench.ts` sanitizes a 1,000-message provider payload on the development host:
152
+
153
+ ```text
154
+ mean 1.75 ms, p99 3.03 ms, max 6.91 ms
155
+ ```
156
+
157
+ This is below the initial 50 ms typical context-operation target, not a portable guarantee.