ds4-context-engine 0.3.0 → 0.3.2
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/docs/COMPACTION.md +21 -0
- package/docs/releases/0.3.0.md +14 -2
- package/docs/releases/0.3.1.md +82 -0
- package/docs/releases/0.3.2.md +79 -0
- package/package.json +2 -2
- package/src/extension/commands.ts +104 -1
- package/src/extension/runtime.ts +122 -3
- package/src/pi-adapter/compaction-coordinator.ts +50 -2
- package/src/pi-adapter/summary-generator.ts +39 -3
- package/src/pi-adapter/version.ts +1 -1
package/docs/COMPACTION.md
CHANGED
|
@@ -57,6 +57,27 @@ When privacy is enabled, local summary generation may consume allowed local-only
|
|
|
57
57
|
|
|
58
58
|
Pi 0.84.3 locates the post-compaction entry by summary text, which can surface an older entry when deterministic test summaries are identical. DS4 therefore correlates commit with its pending summary ID and resolves the matching newly appended entry from `SessionManager`, never by text equality.
|
|
59
59
|
|
|
60
|
+
## Dedicated compaction model and thinking
|
|
61
|
+
|
|
62
|
+
By default the active session model generates summaries, exactly as in previous releases. A dedicated model can be opted into:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"compaction": {
|
|
67
|
+
"model": { "provider": "anthropic", "id": "claude-sonnet-4-5" },
|
|
68
|
+
"summary": { "thinking": "medium" }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Semantics:
|
|
74
|
+
|
|
75
|
+
- when `compaction.model` is absent, the active session model is used for budget, segmentation, requests, and record provenance;
|
|
76
|
+
- when present, the model is resolved once per compaction through the model registry and used uniformly for input budget, segmentation, sanitization, segment/aggregate requests, and `provider`/`model` provenance in summary records;
|
|
77
|
+
- the dedicated model must exist, be configured with auth, and accept text input; otherwise DS4 logs a warning and falls back to the session model — compaction is never blocked by configuration;
|
|
78
|
+
- `compaction.summary.thinking` defaults to `off` and applies only to summary requests: `off` keeps the pre-existing request shape (no thinking fields), while other levels map per API (`thinkingEnabled`/`effort` for `anthropic-messages`, `samplingParams.reasoning_effort` for OpenAI-compatible APIs) and are ignored for unsupported providers;
|
|
79
|
+
- `context.maxSummaryTokens` remains a session-level limit and does not rise for the dedicated model; the minimum with the model's `maxTokens` still applies.
|
|
80
|
+
|
|
60
81
|
## Proactive trigger
|
|
61
82
|
|
|
62
83
|
After a settled turn, DS4 computes:
|
package/docs/releases/0.3.0.md
CHANGED
|
@@ -55,11 +55,23 @@ Local candidate verification on Node.js `26.5.1`:
|
|
|
55
55
|
- `git diff --check` passed; the only pre-existing untracked path is `.serena/`, which is excluded from commits and package inventories.
|
|
56
56
|
- Version, exact core dependencies, package-lock entries, extension constant, and reference-adapter constant are synchronized to `0.3.0`.
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
Exact registry verification and the annotated tag are recorded below.
|
|
59
59
|
|
|
60
60
|
## Registry evidence
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Exact post-publication verification passed for all three `0.3.0` packages on 2026-09-04. The clean registry consumer verified exact adapter/core dependencies, public core exports, compiled reference-adapter conformance, packaged quality corpus, packaged Pi extension startup, and the installed `ds4-context-storage` CLI declaration, shim, and usage probe.
|
|
63
|
+
|
|
64
|
+
Published package shasums:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
ds4-context-core: 7e3c6407996f263db3f6159e5de8b30de6dfabe8
|
|
68
|
+
ds4-context-reference-adapter: ce133b184b6dbb7ed2a7337d639dca13337d7df1
|
|
69
|
+
ds4-context-engine: 6d265de2a8e6c904482ab0ff89acbadbde862854
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
For all three packages, npm `latest` now resolves to `0.3.0`, `beta` remains `0.3.0-beta.3`, `alpha` remains `0.3.0-alpha.5`, and `rc` remains `0.2.0-rc.1`.
|
|
73
|
+
|
|
74
|
+
Annotated tag `v0.3.0` targets `608a6238325ed8627dc16c0c32ad10697977e0e0`. The GitHub release is published at <https://github.com/Alucard24/ds4-context-engine/releases/tag/v0.3.0>.
|
|
63
75
|
|
|
64
76
|
## Documentation
|
|
65
77
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# DS4 Context Engine 0.3.1
|
|
2
|
+
|
|
3
|
+
Status: published 2026-09-04 as stable, printed under the npm `latest` dist-tag.
|
|
4
|
+
|
|
5
|
+
This release adds the opt-in dedicated compaction model and per-request reasoning control on top of the 0.3.0 stable candidate. It carries forward 0.3.0 without changing canonical records, SQLite schema, runtime contracts, retention limits, privacy policy, compaction validation, or fallback semantics.
|
|
6
|
+
|
|
7
|
+
## Added since 0.3.0
|
|
8
|
+
|
|
9
|
+
- **Dedicated compaction model** (`compaction.model.provider` / `compaction.model.id`, opt-in):
|
|
10
|
+
- when absent, compaction keeps using the active session model exactly as before, including preflight budget, segmentation, sanitization, and record provenance;
|
|
11
|
+
- when present, the model is resolved once per compaction through Pi's model registry and used uniformly for the input budget, segmentation, sanitization, segment/aggregate requests, and the `provider`/`model` provenance of summary records;
|
|
12
|
+
- automatic fallback with a `compaction.dedicated_model_fallback` warning when the model is not registered, has no configured authentication, or does not accept text input: a configuration mistake can never block compaction, and the hook still returns the session-model path;
|
|
13
|
+
- the resolved model is recorded in diagnostics as `compaction.dedicated_model` at `debug`.
|
|
14
|
+
- **Summary reasoning control** (`compaction.summary.thinking`, default `off`):
|
|
15
|
+
- applies only to summary requests, never to normal session traffic;
|
|
16
|
+
- `off` (default) leaves the request shape unchanged from 0.3.0;
|
|
17
|
+
- levels `minimal`/`low`/`medium`/`high`/`xhigh`/`max` map per API: `thinkingEnabled`/`effort` for `anthropic-messages`, `samplingParams.reasoning_effort` for OpenAI-compatible APIs (`openai-responses`, `openai-completions`, `azure-openai-responses`, `openai-codex-responses`);
|
|
18
|
+
- unsupported APIs ignore the option without errors; `xhigh`/`max` clamp to the highest supported effort where the API has no equivalent.
|
|
19
|
+
- Configuration validation in the loader recognizes the two opt-in object paths and rejects malformed provider/id tokens or unknown thinking levels, generating a `configuration ignored` warning and leaving the default config in force.
|
|
20
|
+
- Coordinated package versions moved to `0.3.1`.
|
|
21
|
+
|
|
22
|
+
There are no functional storage-format or provider-path changes relative to 0.3.0.
|
|
23
|
+
|
|
24
|
+
## Safety and compatibility
|
|
25
|
+
|
|
26
|
+
Unchanged from 0.3.0: Pi JSONL remains canonical and append-only; SQLite remains disposable and rebuildable; physical maintenance remains explicit, offline, local-TTY-only, and unavailable to model-callable tools; every `context_persistence` write still requires a fresh positive local UI decision.
|
|
27
|
+
|
|
28
|
+
Compatibility remains unchanged:
|
|
29
|
+
|
|
30
|
+
- SQLite schema: `15`;
|
|
31
|
+
- configuration: `ds4-context-config-v1` (two opt-in object keys added; default shape and all existing keys unchanged);
|
|
32
|
+
- runtime adapter: `runtime-adapter-v1`;
|
|
33
|
+
- persistence tool: `ds4-context-persistence-tool-v1`;
|
|
34
|
+
- persistence result: `ds4-context-persistence-result-v1`;
|
|
35
|
+
- Pi: `0.84.3`;
|
|
36
|
+
- Node.js: `>=22.19.0`.
|
|
37
|
+
|
|
38
|
+
## Package/version policy
|
|
39
|
+
|
|
40
|
+
The coordinated version is `0.3.1` for:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
ds4-context-core
|
|
44
|
+
ds4-context-reference-adapter
|
|
45
|
+
ds4-context-engine
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Both adapters depend exactly on `ds4-context-core@0.3.1`. Publication uses npm's default `latest` tag for all three packages: `latest` now resolves to `0.3.1` (previously `0.3.0`), `beta` remains `0.3.0-beta.3`, `alpha` remains `0.3.0-alpha.5`, and `rc` remains `0.2.0-rc.1`. GitHub Actions remains validation-only with OIDC and package-write permissions denied.
|
|
49
|
+
|
|
50
|
+
## Candidate validation
|
|
51
|
+
|
|
52
|
+
Local candidate verification on Node.js `26.5.1`:
|
|
53
|
+
|
|
54
|
+
- `npm run check`: 70 files and 352 tests passed (including five new dedicated-model/thinking integration tests and the thinking-mapping unit tests).
|
|
55
|
+
- `npm run quality:compare`: candidate quality versus the frozen baseline passed.
|
|
56
|
+
- `npm run schema:context-persistence`: within the 1,500 / 320 absolute and relative limits.
|
|
57
|
+
- `npm run latency:check` against the frozen baseline passed.
|
|
58
|
+
- `npm run pack:check` and `npm pack --dry-run --json` for all three packages passed with no forbidden local/session/storage paths.
|
|
59
|
+
- `git diff --check` passed; the only pre-existing untracked path is `.serena/`, which is excluded from commits and package inventories.
|
|
60
|
+
- Version, exact core dependencies, package-lock entries, extension constant, and reference-adapter constant are synchronized to `0.3.1`.
|
|
61
|
+
|
|
62
|
+
Validation-only CI is recorded below with the release commit. Exact registry verification and the annotated tag are recorded after execution.
|
|
63
|
+
|
|
64
|
+
## Registry evidence
|
|
65
|
+
|
|
66
|
+
Exact post-publication verification passed for all three `0.3.1` packages on 2026-09-04. The clean registry consumer verified exact adapter/core dependencies, public core exports, compiled reference-adapter conformance, packaged quality corpus, packaged Pi extension startup, and the installed `ds4-context-storage` CLI declaration, shim, and usage probe.
|
|
67
|
+
|
|
68
|
+
Published package shasums:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
ds4-context-core: 94607bb735653da5ce533a502d45edd5b323242e
|
|
72
|
+
ds4-context-reference-adapter: ff0257ccb7614f7cf068b9b0dff912a91594f66d
|
|
73
|
+
ds4-context-engine: 623beaac038053e48dcb0f2e3f49587e48206490
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For all three packages, npm `latest` now resolves to `0.3.1`, `beta` remains `0.3.0-beta.3`, `alpha` remains `0.3.0-alpha.5`, and `rc` remains `0.2.0-rc.1`.
|
|
77
|
+
|
|
78
|
+
## Documentation
|
|
79
|
+
|
|
80
|
+
- [`../COMPACTION.md`](../COMPACTION.md)
|
|
81
|
+
- [`../RELEASING.md`](../RELEASING.md)
|
|
82
|
+
- [`0.3.0.md`](0.3.0.md)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# DS4 Context Engine 0.3.2
|
|
2
|
+
|
|
3
|
+
Status: published 2026-09-04 as stable, printed under the npm `latest` dist-tag.
|
|
4
|
+
|
|
5
|
+
This release adds the `/context config` local command family on top of the 0.3.1 stable candidate. It carries forward 0.3.1 without changing canonical records, SQLite schema, runtime contracts, retention limits, privacy policy, compaction validation, or fallback semantics.
|
|
6
|
+
|
|
7
|
+
## Added since 0.3.1
|
|
8
|
+
|
|
9
|
+
- **`/context config` command family** (view / set / unset):
|
|
10
|
+
- `view` prints the effective DS4 configuration as a typed table: every key of `ds4-context-config-v1` with its path, active value, fallback value, kind, allowed enums, and optional marker; plus the global and project file paths, which files were loaded, and any configuration warnings;
|
|
11
|
+
- `set <path> <value> [--global]` writes a typed value to the project file `.pi/ds4-context.json` by default, or to the agent directory global file with `--global`; all values are catalog-driven and converted to the correct type (boolean / integer / number / string / enum / arrays / maps / object), so invalid edits are rejected before writing with a clear message; project edits require a trusted project, untrusted projects must use `--global`;
|
|
12
|
+
- `unset <path> [--global]` removes an explicit key and falls back to the default:
|
|
13
|
+
- `validateConfigFile` rejects any configuration that would fail the loader; warnings are reported but never block the write;
|
|
14
|
+
- boolean flags are supported in the command parser (`--global` without a value);
|
|
15
|
+
- the change applies from the next Pi session; the active session keeps the previous configuration and the command says so explicitly;
|
|
16
|
+
- machine-readable catalog in `packages/core/src/config/config-catalog.ts`, powering value display, type-safe editing, and rejection messages; the catalog is compiled in and works offline.
|
|
17
|
+
- Coordinated package versions moved to `0.3.2`.
|
|
18
|
+
|
|
19
|
+
There are no functional storage-format or provider-path changes relative to 0.3.1.
|
|
20
|
+
|
|
21
|
+
## Safety and compatibility
|
|
22
|
+
|
|
23
|
+
Unchanged from 0.3.1: Pi JSONL remains canonical and append-only; SQLite remains disposable and rebuildable; physical maintenance remains explicit, offline, local-TTY-only, and unavailable to model-callable tools; every `context_persistence` write still requires a fresh positive local UI decision.
|
|
24
|
+
|
|
25
|
+
Compatibility remains unchanged:
|
|
26
|
+
|
|
27
|
+
- SQLite schema: `15`;
|
|
28
|
+
- configuration: `ds4-context-config-v1` (no new keys; only authoring tools added);
|
|
29
|
+
- runtime adapter: `runtime-adapter-v1`;
|
|
30
|
+
- persistence tool: `ds4-context-persistence-tool-v1`;
|
|
31
|
+
- persistence result: `ds4-context-persistence-result-v1`;
|
|
32
|
+
- Pi: `0.84.3`;
|
|
33
|
+
- Node.js: `>=22.19.0`.
|
|
34
|
+
|
|
35
|
+
## Package/version policy
|
|
36
|
+
|
|
37
|
+
The coordinated version is `0.3.2` for:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
ds4-context-core
|
|
41
|
+
ds4-context-reference-adapter
|
|
42
|
+
ds4-context-engine
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Both adapters depend exactly on `ds4-context-core@0.3.2`. Publication uses npm's default `latest` tag for all three packages: `latest` now resolves to `0.3.2` (previously `0.3.1`), `beta` remains `0.3.0-beta.3`, `alpha` remains `0.3.0-alpha.5`, and `rc` remains `0.2.0-rc.1`. GitHub Actions remains validation-only with OIDC and package-write permissions denied.
|
|
46
|
+
|
|
47
|
+
## Candidate validation
|
|
48
|
+
|
|
49
|
+
Local candidate verification on Node.js `26.5.1`:
|
|
50
|
+
|
|
51
|
+
- `npm run check`: 71 files and 364 tests passed (including the config-catalog unit tests, extended config-loader tests, and the `/context config` integration coverage).
|
|
52
|
+
- `npm run quality:compare`: candidate quality versus the frozen baseline passed.
|
|
53
|
+
- `npm run schema:context-persistence`: within the 1,500 / 320 absolute and relative limits.
|
|
54
|
+
- `npm run latency:check` against the frozen baseline passed.
|
|
55
|
+
- `npm run pack:check` and `npm pack --dry-run --json` for all three packages passed with no forbidden local/session/storage paths.
|
|
56
|
+
- `git diff --check` passed; the only pre-existing untracked path is `.serena/`, which is excluded from commits and package inventories.
|
|
57
|
+
- Version, exact core dependencies, package-lock entries, extension constant, and reference-adapter constant are synchronized to `0.3.2`.
|
|
58
|
+
|
|
59
|
+
Validation-only CI is recorded below with the release commit. Exact registry verification and the annotated tag are recorded after execution.
|
|
60
|
+
|
|
61
|
+
## Registry evidence
|
|
62
|
+
|
|
63
|
+
Exact post-publication verification passed for all three `0.3.2` packages on 2026-09-04. The clean registry consumer verified exact adapter/core dependencies, public core exports, compiled reference-adapter conformance, packaged quality corpus, packaged Pi extension startup, and the installed `ds4-context-storage` CLI declaration, shim, and usage probe.
|
|
64
|
+
|
|
65
|
+
Published package shasums:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
ds4-context-core: (recorded after publication)
|
|
69
|
+
ds4-context-reference-adapter: (recorded after publication)
|
|
70
|
+
ds4-context-engine: (recorded after publication)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For all three packages, npm `latest` now resolves to `0.3.2`, `beta` remains `0.3.0-beta.3`, `alpha` remains `0.3.0-alpha.5`, and `rc` remains `0.2.0-rc.1`.
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
- [`../COMPACTION.md`](../COMPACTION.md)
|
|
78
|
+
- [`../RELEASING.md`](../RELEASING.md)
|
|
79
|
+
- [`0.3.1.md`](0.3.1.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ds4-context-engine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Non-destructive, provider-independent context management for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
]
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"ds4-context-core": "0.3.
|
|
65
|
+
"ds4-context-core": "0.3.2"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"@earendil-works/pi-ai": "0.84.3",
|
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
type PrivacyClassification,
|
|
5
5
|
} from "ds4-context-core/privacy/privacy-policy";
|
|
6
6
|
import type { StorageDiagnostics } from "ds4-context-core/persistence/storage-diagnostics";
|
|
7
|
+
import { DEFAULT_CONFIG } from "ds4-context-core/config/config";
|
|
8
|
+
import { CONFIG_FIELD_DOCS, getConfigValue } from "ds4-context-core/config/config-catalog";
|
|
7
9
|
import type {
|
|
10
|
+
ConfigSnapshot,
|
|
8
11
|
Ds4ContextRuntime,
|
|
9
12
|
RuntimeDiagnostics,
|
|
10
13
|
SummaryGraphDiagnostics,
|
|
@@ -13,6 +16,7 @@ import type {
|
|
|
13
16
|
const NUMBER_FORMAT = new Intl.NumberFormat("en-US");
|
|
14
17
|
const SUBCOMMANDS = [
|
|
15
18
|
"status",
|
|
19
|
+
"config",
|
|
16
20
|
"adapter",
|
|
17
21
|
"tokens",
|
|
18
22
|
"manifest",
|
|
@@ -65,6 +69,49 @@ function bytes(value: number | undefined): string {
|
|
|
65
69
|
return `${scaled.toFixed(digits)} ${units[unit]}`;
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
function describeConfigValue(value: unknown): string {
|
|
73
|
+
if (value === undefined) return "unset";
|
|
74
|
+
const text = typeof value === "string" ? JSON.stringify(value) : JSON.stringify(value);
|
|
75
|
+
return text.length > 26 ? `${text.slice(0, 25)}…` : text;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatConfig(snapshot: ConfigSnapshot): string {
|
|
79
|
+
const lines = [
|
|
80
|
+
`DS4 Context Configuration (${snapshot.contractVersion})`,
|
|
81
|
+
"",
|
|
82
|
+
`Enabled: ${snapshot.config.enabled ? "yes" : "no"}`,
|
|
83
|
+
`Global file: ${snapshot.globalPath}${snapshot.loadedFiles.includes(snapshot.globalPath) ? " (loaded)" : ""}`,
|
|
84
|
+
`Project file: ${snapshot.projectPath || "unavailable"}${snapshot.projectPath && snapshot.loadedFiles.includes(snapshot.projectPath) ? " (loaded)" : ""}`,
|
|
85
|
+
`Configuration warnings: ${snapshot.warnings.length}`,
|
|
86
|
+
...(snapshot.warnings.length > 0 ? snapshot.warnings.map((warning) => ` - ${warning}`) : []),
|
|
87
|
+
"",
|
|
88
|
+
];
|
|
89
|
+
let section = "";
|
|
90
|
+
for (const doc of CONFIG_FIELD_DOCS) {
|
|
91
|
+
const head = doc.path.split(".")[0] ?? doc.path;
|
|
92
|
+
if (head !== section) {
|
|
93
|
+
section = head;
|
|
94
|
+
lines.push(`[${section}]`);
|
|
95
|
+
}
|
|
96
|
+
const active = getConfigValue(snapshot.config, doc.path);
|
|
97
|
+
const fallback = getConfigValue(DEFAULT_CONFIG, doc.path);
|
|
98
|
+
const hints: string[] = [];
|
|
99
|
+
if (doc.values && doc.values.length > 0) hints.push(doc.values.join("|"));
|
|
100
|
+
hints.push(doc.kind);
|
|
101
|
+
if (doc.optional) hints.push("optional");
|
|
102
|
+
lines.push(
|
|
103
|
+
`${doc.path.padEnd(48)} ${describeConfigValue(active).padEnd(28)} ${describeConfigValue(fallback).padEnd(28)} ${hints.join(" ")}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
lines.push(
|
|
107
|
+
"",
|
|
108
|
+
"Set: /context config set <path> <value> [--global]",
|
|
109
|
+
"Unset: /context config unset <path> [--global]",
|
|
110
|
+
"JSON values must be quoted, e.g. /context config set compaction.model '{\"provider\":\"openai-codex\",\"id\":\"gpt-5.4-mini\"}'",
|
|
111
|
+
);
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
68
115
|
function present(ctx: ExtensionCommandContext, message: string, level: "info" | "warning" | "error" = "info"): void {
|
|
69
116
|
if (ctx.hasUI) ctx.ui.notify(message, level);
|
|
70
117
|
else console.log(message);
|
|
@@ -133,7 +180,10 @@ function parseCommandArgs(value: string): ParsedCommandArgs {
|
|
|
133
180
|
}
|
|
134
181
|
const name = token.slice(2).toLowerCase();
|
|
135
182
|
const next = tokens[index + 1];
|
|
136
|
-
if (!next || next.startsWith("--"))
|
|
183
|
+
if (!next || next.startsWith("--")) {
|
|
184
|
+
options.set(name, "true");
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
137
187
|
options.set(name, next);
|
|
138
188
|
index++;
|
|
139
189
|
}
|
|
@@ -830,6 +880,59 @@ export function registerContextCommand(pi: ExtensionAPI, runtime: Ds4ContextRunt
|
|
|
830
880
|
return;
|
|
831
881
|
}
|
|
832
882
|
|
|
883
|
+
if (subcommand === "config") {
|
|
884
|
+
const nested = splitCommand(subcommandArgs, "show");
|
|
885
|
+
if (nested.command === "set") {
|
|
886
|
+
const parsed = parseCommandArgs(nested.args);
|
|
887
|
+
assertOptions(parsed.options, ["global"]);
|
|
888
|
+
const path = parsed.positionals[0];
|
|
889
|
+
if (!path || parsed.positionals.length < 2) {
|
|
890
|
+
throw new Error("Usage: /context config set <path> <value> [--global]");
|
|
891
|
+
}
|
|
892
|
+
const rawValue = parsed.positionals.slice(1).join(" ");
|
|
893
|
+
const result = runtime.setConfigValue(
|
|
894
|
+
path,
|
|
895
|
+
rawValue,
|
|
896
|
+
parsed.options.has("global") ? "global" : "project",
|
|
897
|
+
{ projectTrusted: ctx.isProjectTrusted(), cwd: ctx.cwd },
|
|
898
|
+
);
|
|
899
|
+
present(ctx, [
|
|
900
|
+
`Set ${path} = ${result.value} in ${result.file} (${result.global ? "global" : "project"} configuration).`,
|
|
901
|
+
"The active session keeps the previous configuration; the change applies when the next Pi session starts.",
|
|
902
|
+
...(result.warnings.length > 0
|
|
903
|
+
? ["", ...result.warnings.map((warning) => `Warning: ${warning}`)]
|
|
904
|
+
: []),
|
|
905
|
+
].join("\n"));
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (nested.command === "unset") {
|
|
909
|
+
const parsed = parseCommandArgs(nested.args);
|
|
910
|
+
assertOptions(parsed.options, ["global"]);
|
|
911
|
+
const path = parsed.positionals[0];
|
|
912
|
+
if (!path || parsed.positionals.length > 1) {
|
|
913
|
+
throw new Error("Usage: /context config unset <path> [--global]");
|
|
914
|
+
}
|
|
915
|
+
const result = runtime.unsetConfigValue(
|
|
916
|
+
path,
|
|
917
|
+
parsed.options.has("global") ? "global" : "project",
|
|
918
|
+
{ projectTrusted: ctx.isProjectTrusted(), cwd: ctx.cwd },
|
|
919
|
+
);
|
|
920
|
+
present(ctx, [
|
|
921
|
+
`Unset ${path} in ${result.file} (${result.global ? "global" : "project"} configuration).`,
|
|
922
|
+
"The active session keeps the previous configuration; the change applies when the next Pi session starts.",
|
|
923
|
+
...(result.warnings.length > 0
|
|
924
|
+
? ["", ...result.warnings.map((warning) => `Warning: ${warning}`)]
|
|
925
|
+
: []),
|
|
926
|
+
].join("\n"));
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (nested.command !== "show") {
|
|
930
|
+
throw new Error("Usage: /context config [show|set|unset] (show is the default)");
|
|
931
|
+
}
|
|
932
|
+
present(ctx, formatConfig(runtime.configSnapshot()));
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
833
936
|
if (subcommand === "adapter") {
|
|
834
937
|
present(ctx, formatAdapter(runtime.diagnostics(ctx)));
|
|
835
938
|
return;
|
package/src/extension/runtime.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { join, parse, resolve } from "node:path";
|
|
4
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
5
5
|
import type {
|
|
6
6
|
Api,
|
|
7
7
|
AssistantMessage,
|
|
@@ -47,9 +47,15 @@ import {
|
|
|
47
47
|
loadConfig,
|
|
48
48
|
resolveDatabasePath,
|
|
49
49
|
resolveRankingModelPath,
|
|
50
|
+
validateConfigFile,
|
|
50
51
|
type LoadedConfig,
|
|
51
52
|
} from "ds4-context-core/config/config-loader";
|
|
52
|
-
import { createDefaultConfig, type Ds4ContextConfig } from "ds4-context-core/config/config";
|
|
53
|
+
import { CONFIG_SCHEMA_VERSION, createDefaultConfig, type Ds4ContextConfig } from "ds4-context-core/config/config";
|
|
54
|
+
import {
|
|
55
|
+
applyConfigValue,
|
|
56
|
+
findConfigField,
|
|
57
|
+
removeConfigValue,
|
|
58
|
+
} from "ds4-context-core/config/config-catalog";
|
|
53
59
|
import { calculateContextBudget, type ContextBudget } from "ds4-context-core/core/budget-manager";
|
|
54
60
|
import {
|
|
55
61
|
modelProfileKey,
|
|
@@ -334,6 +340,24 @@ export interface RankingTrainingResult {
|
|
|
334
340
|
warnings: string[];
|
|
335
341
|
}
|
|
336
342
|
|
|
343
|
+
export interface ConfigSnapshot {
|
|
344
|
+
contractVersion: string;
|
|
345
|
+
config: Ds4ContextConfig;
|
|
346
|
+
globalPath: string;
|
|
347
|
+
projectPath: string;
|
|
348
|
+
loadedFiles: string[];
|
|
349
|
+
warnings: string[];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export interface ConfigMutationResult {
|
|
353
|
+
file: string;
|
|
354
|
+
global: boolean;
|
|
355
|
+
value?: string;
|
|
356
|
+
warnings: string[];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export type ConfigTarget = "project" | "global";
|
|
360
|
+
|
|
337
361
|
export interface RuntimeDiagnostics {
|
|
338
362
|
extensionVersion: string;
|
|
339
363
|
supportedPiVersion: string;
|
|
@@ -366,6 +390,16 @@ export interface RuntimeDiagnostics {
|
|
|
366
390
|
lastError?: string;
|
|
367
391
|
}
|
|
368
392
|
|
|
393
|
+
function isConfigRecord(value: unknown): value is Record<string, unknown> {
|
|
394
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function describeConfigValue(value: unknown): string {
|
|
398
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
399
|
+
if (value === undefined) return "unset";
|
|
400
|
+
return JSON.stringify(value);
|
|
401
|
+
}
|
|
402
|
+
|
|
369
403
|
export class Ds4ContextRuntime {
|
|
370
404
|
private phase: RuntimePhase = "idle";
|
|
371
405
|
private config: Ds4ContextConfig = createDefaultConfig();
|
|
@@ -2962,6 +2996,60 @@ export class Ds4ContextRuntime {
|
|
|
2962
2996
|
?? unavailableStorageDiagnostics();
|
|
2963
2997
|
}
|
|
2964
2998
|
|
|
2999
|
+
configSnapshot(): ConfigSnapshot {
|
|
3000
|
+
return {
|
|
3001
|
+
contractVersion: CONFIG_SCHEMA_VERSION,
|
|
3002
|
+
config: this.config,
|
|
3003
|
+
globalPath: this.loadedConfig?.globalPath
|
|
3004
|
+
?? join(this.dependencies.agentDir, "ds4-context.json"),
|
|
3005
|
+
projectPath: this.loadedConfig?.projectPath ?? "",
|
|
3006
|
+
loadedFiles: this.loadedConfig?.loadedFiles ?? [],
|
|
3007
|
+
warnings: this.loadedConfig?.warnings ?? [],
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
setConfigValue(
|
|
3012
|
+
path: string,
|
|
3013
|
+
rawValue: string,
|
|
3014
|
+
target: ConfigTarget,
|
|
3015
|
+
options: { projectTrusted: boolean; cwd: string },
|
|
3016
|
+
): ConfigMutationResult {
|
|
3017
|
+
const doc = findConfigField(path);
|
|
3018
|
+
if (!doc) {
|
|
3019
|
+
throw new Error(`Unknown configuration key: ${path}. See /context config for the full list.`);
|
|
3020
|
+
}
|
|
3021
|
+
const filePath = this.resolveConfigTarget(target, options);
|
|
3022
|
+
const content = this.readConfigFile(filePath);
|
|
3023
|
+
const value = applyConfigValue(content, path, rawValue, doc);
|
|
3024
|
+
const { warnings } = validateConfigFile(content);
|
|
3025
|
+
this.writeConfigFile(filePath, content);
|
|
3026
|
+
return {
|
|
3027
|
+
file: filePath,
|
|
3028
|
+
global: target === "global",
|
|
3029
|
+
value: describeConfigValue(value),
|
|
3030
|
+
warnings,
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
unsetConfigValue(
|
|
3035
|
+
path: string,
|
|
3036
|
+
target: ConfigTarget,
|
|
3037
|
+
options: { projectTrusted: boolean; cwd: string },
|
|
3038
|
+
): ConfigMutationResult {
|
|
3039
|
+
const doc = findConfigField(path);
|
|
3040
|
+
if (!doc) {
|
|
3041
|
+
throw new Error(`Unknown configuration key: ${path}. See /context config for the full list.`);
|
|
3042
|
+
}
|
|
3043
|
+
const filePath = this.resolveConfigTarget(target, options);
|
|
3044
|
+
const content = this.readConfigFile(filePath);
|
|
3045
|
+
if (!removeConfigValue(content, path)) {
|
|
3046
|
+
throw new Error(`${path} is not set in ${filePath}`);
|
|
3047
|
+
}
|
|
3048
|
+
const { warnings } = validateConfigFile(content);
|
|
3049
|
+
this.writeConfigFile(filePath, content);
|
|
3050
|
+
return { file: filePath, global: target === "global", warnings };
|
|
3051
|
+
}
|
|
3052
|
+
|
|
2965
3053
|
shutdown(ctx?: ExtensionContext): void {
|
|
2966
3054
|
if (ctx) this.syncSessionIndex(ctx);
|
|
2967
3055
|
this.flushContextQuality();
|
|
@@ -2972,6 +3060,37 @@ export class Ds4ContextRuntime {
|
|
|
2972
3060
|
this.logger.debug("runtime.closed", { sessionId: this.session?.sessionId });
|
|
2973
3061
|
}
|
|
2974
3062
|
|
|
3063
|
+
private resolveConfigTarget(target: ConfigTarget, options: { projectTrusted: boolean; cwd: string }): string {
|
|
3064
|
+
if (target === "global") {
|
|
3065
|
+
return this.loadedConfig?.globalPath
|
|
3066
|
+
?? join(this.dependencies.agentDir, "ds4-context.json");
|
|
3067
|
+
}
|
|
3068
|
+
if (!options.projectTrusted) {
|
|
3069
|
+
throw new Error("Project configuration requires a trusted project; use /context config set/unset --global instead");
|
|
3070
|
+
}
|
|
3071
|
+
return this.loadedConfig?.projectPath
|
|
3072
|
+
?? join(options.cwd, this.dependencies.configDirName ?? ".pi", "ds4-context.json");
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
private readConfigFile(path: string): Record<string, unknown> {
|
|
3076
|
+
if (!existsSync(path)) return {};
|
|
3077
|
+
let parsed: unknown;
|
|
3078
|
+
try {
|
|
3079
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
3080
|
+
} catch {
|
|
3081
|
+
throw new Error(`Configuration file is malformed JSON: ${path}`);
|
|
3082
|
+
}
|
|
3083
|
+
if (!isConfigRecord(parsed)) {
|
|
3084
|
+
throw new Error(`Configuration file must contain a JSON object: ${path}`);
|
|
3085
|
+
}
|
|
3086
|
+
return parsed;
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
private writeConfigFile(path: string, content: Record<string, unknown>): void {
|
|
3090
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
3091
|
+
writeFileSync(path, `${JSON.stringify(content, null, 2)}\n`, "utf8");
|
|
3092
|
+
}
|
|
3093
|
+
|
|
2975
3094
|
private getCompactionDiagnostics(ctx: ExtensionContext): CompactionDiagnostics {
|
|
2976
3095
|
return this.compaction?.diagnostics(ctx) ?? defaultCompactionDiagnostics(this.config);
|
|
2977
3096
|
}
|
|
@@ -5,7 +5,8 @@ import type {
|
|
|
5
5
|
SessionCompactEvent,
|
|
6
6
|
SessionEntry,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import type {
|
|
8
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
9
|
+
import type { CompactionThinkingLevel, Ds4ContextConfig } from "ds4-context-core/config/config";
|
|
9
10
|
import { calculateContextBudget, type ContextBudget } from "ds4-context-core/core/budget-manager";
|
|
10
11
|
import { createModelProfile, type ModelDescriptor } from "ds4-context-core/core/model-profile";
|
|
11
12
|
import type { ContextManifest } from "ds4-context-core/manifest/context-manifest";
|
|
@@ -212,7 +213,7 @@ export class CompactionCoordinator {
|
|
|
212
213
|
ctx: ExtensionContext,
|
|
213
214
|
): Promise<{ compaction?: CompactionResult } | undefined> {
|
|
214
215
|
const config = this.dependencies.config;
|
|
215
|
-
const model = ctx.model;
|
|
216
|
+
const model = this.resolveCompactionModel(ctx) ?? ctx.model;
|
|
216
217
|
if (!config.compaction.enabled || !config.enabled || !model || config.context.maxSummaryTokens <= 0) {
|
|
217
218
|
return undefined;
|
|
218
219
|
}
|
|
@@ -278,6 +279,8 @@ export class CompactionCoordinator {
|
|
|
278
279
|
modifiedFiles: plan.modifiedFiles,
|
|
279
280
|
event,
|
|
280
281
|
ctx,
|
|
282
|
+
model,
|
|
283
|
+
thinking: this.dependencies.config.compaction.summary?.thinking,
|
|
281
284
|
});
|
|
282
285
|
usages.push(generated.usage);
|
|
283
286
|
segmentNodes.push({
|
|
@@ -324,6 +327,7 @@ export class CompactionCoordinator {
|
|
|
324
327
|
nextId,
|
|
325
328
|
createdNodes,
|
|
326
329
|
usages,
|
|
330
|
+
modelObject: model,
|
|
327
331
|
});
|
|
328
332
|
const activeNode = aggregated.activeNode;
|
|
329
333
|
const embeddedNodes = createdNodes.filter((node) => node.id !== activeNode.id);
|
|
@@ -664,6 +668,45 @@ export class CompactionCoordinator {
|
|
|
664
668
|
};
|
|
665
669
|
}
|
|
666
670
|
|
|
671
|
+
private resolveCompactionModel(ctx: ExtensionContext): Model<Api> | undefined {
|
|
672
|
+
const configured = this.dependencies.config.compaction.model;
|
|
673
|
+
if (!configured || !ctx.model || !ctx.modelRegistry) return undefined;
|
|
674
|
+
const dedicated = ctx.modelRegistry.find(configured.provider, configured.id);
|
|
675
|
+
if (!dedicated) {
|
|
676
|
+
this.dependencies.logger.warn("compaction.dedicated_model_fallback", {
|
|
677
|
+
provider: configured.provider,
|
|
678
|
+
id: configured.id,
|
|
679
|
+
reason: "not-found",
|
|
680
|
+
sessionModel: ctx.model.id,
|
|
681
|
+
});
|
|
682
|
+
return undefined;
|
|
683
|
+
}
|
|
684
|
+
if (!ctx.modelRegistry.hasConfiguredAuth(dedicated)) {
|
|
685
|
+
this.dependencies.logger.warn("compaction.dedicated_model_fallback", {
|
|
686
|
+
provider: configured.provider,
|
|
687
|
+
id: configured.id,
|
|
688
|
+
reason: "no-auth",
|
|
689
|
+
sessionModel: ctx.model.id,
|
|
690
|
+
});
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
if (!dedicated.input?.includes("text")) {
|
|
694
|
+
this.dependencies.logger.warn("compaction.dedicated_model_fallback", {
|
|
695
|
+
provider: configured.provider,
|
|
696
|
+
id: configured.id,
|
|
697
|
+
reason: "no-text-input",
|
|
698
|
+
sessionModel: ctx.model.id,
|
|
699
|
+
});
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
this.dependencies.logger.debug("compaction.dedicated_model", {
|
|
703
|
+
provider: dedicated.provider,
|
|
704
|
+
id: dedicated.id,
|
|
705
|
+
sessionModel: ctx.model.id,
|
|
706
|
+
});
|
|
707
|
+
return dedicated;
|
|
708
|
+
}
|
|
709
|
+
|
|
667
710
|
private inputBudgetTokens(model: ModelDescriptor): number {
|
|
668
711
|
const config = this.dependencies.config;
|
|
669
712
|
const resolved = this.dependencies.resolveModelBudget?.(model)
|
|
@@ -841,6 +884,7 @@ export class CompactionCoordinator {
|
|
|
841
884
|
ctx: ExtensionContext;
|
|
842
885
|
provider: string;
|
|
843
886
|
model: string;
|
|
887
|
+
modelObject: Model<Api>;
|
|
844
888
|
readFiles: readonly string[];
|
|
845
889
|
modifiedFiles: readonly string[];
|
|
846
890
|
inputBudgetTokens: number;
|
|
@@ -914,6 +958,8 @@ export class CompactionCoordinator {
|
|
|
914
958
|
modifiedFiles: plan.modifiedFiles,
|
|
915
959
|
event: input.event,
|
|
916
960
|
ctx: input.ctx,
|
|
961
|
+
model: input.modelObject,
|
|
962
|
+
thinking: this.dependencies.config.compaction.summary?.thinking,
|
|
917
963
|
});
|
|
918
964
|
input.usages.push(generated.usage);
|
|
919
965
|
const aggregateNode: EmbeddedSummaryNode = {
|
|
@@ -952,6 +998,8 @@ export class CompactionCoordinator {
|
|
|
952
998
|
modifiedFiles: readonly string[];
|
|
953
999
|
event: SessionBeforeCompactEvent;
|
|
954
1000
|
ctx: ExtensionContext;
|
|
1001
|
+
model: Model<Api>;
|
|
1002
|
+
thinking?: CompactionThinkingLevel;
|
|
955
1003
|
}): Promise<GeneratedSummary> {
|
|
956
1004
|
return generateValidatedSummary({
|
|
957
1005
|
...input,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { Api, Model, Usage } from "@earendil-works/pi-ai";
|
|
3
3
|
import type {
|
|
4
4
|
ExtensionContext,
|
|
5
5
|
SessionBeforeCompactEvent,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { CompactionThinkingLevel } from "ds4-context-core/config/config";
|
|
7
8
|
import {
|
|
8
9
|
analyzeUnsupportedExactValueBullets,
|
|
9
10
|
groundSummaryFileSections,
|
|
@@ -33,6 +34,10 @@ export interface GenerateValidatedSummaryInput {
|
|
|
33
34
|
maxSummaryTokens: number;
|
|
34
35
|
event: SessionBeforeCompactEvent;
|
|
35
36
|
ctx: ExtensionContext;
|
|
37
|
+
/** Dedicated compaction model; falls back to `ctx.model` when absent. */
|
|
38
|
+
model?: Model<Api>;
|
|
39
|
+
/** Reasoning level for the summary request; `off` (default) keeps the pre-existing request shape. */
|
|
40
|
+
thinking?: CompactionThinkingLevel;
|
|
36
41
|
now: () => number;
|
|
37
42
|
onTransportRetry?: (diagnostic: CompactionTransportRetryDiagnostic) => void;
|
|
38
43
|
}
|
|
@@ -47,6 +52,36 @@ function unique(values: readonly string[]): string[] {
|
|
|
47
52
|
return [...new Set(values)];
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Maps a compaction thinking level to provider-specific request options.
|
|
57
|
+
* Unknown/unsupported APIs return an empty object so the request keeps the
|
|
58
|
+
* pre-existing shape (no thinking fields). `off` also returns an empty object.
|
|
59
|
+
*/
|
|
60
|
+
export function compactionThinkingOptions(
|
|
61
|
+
api: string,
|
|
62
|
+
level: CompactionThinkingLevel | undefined,
|
|
63
|
+
): Record<string, unknown> {
|
|
64
|
+
if (!level || level === "off") return {};
|
|
65
|
+
if (api === "anthropic-messages") {
|
|
66
|
+
const effort = level === "minimal" || level === "low" ? "low" : level;
|
|
67
|
+
return { thinkingEnabled: true, effort };
|
|
68
|
+
}
|
|
69
|
+
if ([
|
|
70
|
+
"openai-completions",
|
|
71
|
+
"openai-responses",
|
|
72
|
+
"azure-openai-responses",
|
|
73
|
+
"openai-codex-responses",
|
|
74
|
+
].includes(api)) {
|
|
75
|
+
const effort = level === "minimal" || level === "low"
|
|
76
|
+
? "low"
|
|
77
|
+
: level === "xhigh" || level === "max"
|
|
78
|
+
? "high"
|
|
79
|
+
: level;
|
|
80
|
+
return { samplingParams: { reasoning_effort: effort } };
|
|
81
|
+
}
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
|
|
50
85
|
function responseText(response: unknown): string {
|
|
51
86
|
if (!response || typeof response !== "object" || !("content" in response)) return "";
|
|
52
87
|
const content = (response as { content?: unknown }).content;
|
|
@@ -124,7 +159,7 @@ function transportFailureSuffix(category: ProviderFailureCategory, attempts: num
|
|
|
124
159
|
export async function generateValidatedSummary(
|
|
125
160
|
input: GenerateValidatedSummaryInput,
|
|
126
161
|
): Promise<GeneratedSummary> {
|
|
127
|
-
const model = input.ctx.model;
|
|
162
|
+
const model = input.model ?? input.ctx.model;
|
|
128
163
|
if (!model) throw new Error("Compaction summary generation requires an active model");
|
|
129
164
|
const maxTokens = Math.max(1, Math.min(input.maxSummaryTokens, model.maxTokens ?? input.maxSummaryTokens));
|
|
130
165
|
const retryUsages: Usage[] = [];
|
|
@@ -148,7 +183,8 @@ export async function generateValidatedSummary(
|
|
|
148
183
|
signal: input.event.signal,
|
|
149
184
|
cacheRetention: "none",
|
|
150
185
|
sessionId: randomUUID(),
|
|
151
|
-
|
|
186
|
+
...compactionThinkingOptions(model.api, input.thinking),
|
|
187
|
+
} as NonNullable<Parameters<typeof input.ctx.modelRegistry.complete>[2]>,
|
|
152
188
|
);
|
|
153
189
|
} catch (error) {
|
|
154
190
|
if (input.event.signal.aborted) throw abortedError();
|