prime-agent-dsh 0.2.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,161 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { readdirSync, statSync } from "node:fs";
3
+ import { basename, join } from "node:path";
4
+ import { RecursiveContextLoader } from "../src/recursive-context-loader.js";
5
+ import { RlmContextInheritance } from "../src/rlm-context-bootstrap.js";
6
+ import { ShadowContextExtension } from "./shadow-context.js";
7
+
8
+ /** Number of committed message entries in the current session (0 = fresh). */
9
+ function committedMessages(ctx: ExtensionContext): number {
10
+ try {
11
+ return ctx.sessionManager.getBranch().filter((entry) => (entry as { type?: string }).type === "message").length;
12
+ } catch {
13
+ return 0;
14
+ }
15
+ }
16
+
17
+ /** Most recent sibling session file (same dir, older than this one), if fresh. */
18
+ function recentSiblingSession(ctx: ExtensionContext): { id: string; ageMinutes: number } | undefined {
19
+ try {
20
+ const dir = ctx.sessionManager.getSessionDir();
21
+ const current = ctx.sessionManager.getSessionFile();
22
+ if (!dir || !current) return undefined;
23
+ const now = Date.now();
24
+ let best: { id: string; mtime: number } | undefined;
25
+ for (const name of readdirSync(dir)) {
26
+ if (!name.endsWith(".jsonl")) continue;
27
+ const full = join(dir, name);
28
+ if (full === current) continue;
29
+ let mtime: number;
30
+ try { mtime = statSync(full).mtimeMs; } catch { continue; }
31
+ if (!best || mtime > best.mtime) best = { id: basename(name, ".jsonl"), mtime };
32
+ }
33
+ if (!best || best.mtime >= now) return undefined;
34
+ const ageMinutes = Math.round((now - best.mtime) / 60000);
35
+ return ageMinutes <= 720 ? { id: best.id, ageMinutes } : undefined;
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ }
40
+
41
+ export const DSH_VERSION = "0.2.0";
42
+ const CACHE_STATUS_KEY = "prime-agent-dsh-cache";
43
+ const CACHE_WIDGET_KEY = "prime-agent-dsh-cache-widget";
44
+ const INSTALLS_KEY = Symbol.for("prime-agent-dsh.installs.v1");
45
+
46
+ export function defaultCacheDisplay(env: NodeJS.ProcessEnv = process.env): boolean {
47
+ const value = env.PRIME_DSH_CACHE_DISPLAY?.trim().toLowerCase();
48
+ return value !== "off" && value !== "false" && value !== "0";
49
+ }
50
+
51
+ type InstallRegistry = WeakSet<object>;
52
+ type GlobalWithDshInstalls = typeof globalThis & { [INSTALLS_KEY]?: InstallRegistry };
53
+
54
+ /** Process-global because separately loaded package copies do not share module state. */
55
+ export function claimExtensionApi(pi: ExtensionAPI): boolean {
56
+ const global = globalThis as GlobalWithDshInstalls;
57
+ const installs = global[INSTALLS_KEY] ??= new WeakSet<object>();
58
+ if (installs.has(pi)) return false;
59
+ installs.add(pi);
60
+ return true;
61
+ }
62
+
63
+ function efficiencyText(value: number | null | undefined): string {
64
+ return value === null || value === undefined ? "—" : `${(value * 100).toFixed(1)}%`;
65
+ }
66
+
67
+ export function cacheFooterText(turnEfficiency: number | null | undefined, sessionEfficiency: number | null | undefined): string {
68
+ return `DSH cache · turn ${efficiencyText(turnEfficiency)} · session ${efficiencyText(sessionEfficiency)}`;
69
+ }
70
+
71
+ function canonicalSessionEfficiency(scope: ReturnType<RecursiveContextLoader["status"]>): number | null {
72
+ const metrics = scope?.lastSync?.manifest.metrics;
73
+ if (!metrics) return scope?.cache?.efficiency ?? null;
74
+ const total = metrics.inputTokens + metrics.cacheReadTokens;
75
+ return total > 0 ? metrics.cacheReadTokens / total : null;
76
+ }
77
+
78
+ function updateCacheStatus(ctx: ExtensionContext, loader: RecursiveContextLoader): void {
79
+ const scope = loader.status(ctx);
80
+ const text = cacheFooterText(scope?.latestCache?.efficiency, canonicalSessionEfficiency(scope));
81
+ ctx.ui.setStatus(CACHE_STATUS_KEY, text);
82
+ ctx.ui.setWidget(CACHE_WIDGET_KEY, [text], { placement: "aboveEditor" });
83
+ }
84
+
85
+ /**
86
+ * Prime owns the model loop, tools, transcript, and RLM tree. DSH contributes a
87
+ * rebuildable context projection, immutable Python-visible artifacts, cache
88
+ * observations. Prime alone owns compaction and DSH only indexes committed history.
89
+ */
90
+ export default function deepSeekHarnessExtension(pi: ExtensionAPI): void {
91
+ if (!claimExtensionApi(pi)) return;
92
+ let showCacheDisplay = defaultCacheDisplay();
93
+ new ShadowContextExtension(pi).register(false);
94
+ const inheritance = new RlmContextInheritance();
95
+ inheritance.register(pi);
96
+ const contextLoader = new RecursiveContextLoader();
97
+ contextLoader.register(pi);
98
+ const refreshCacheDisplay = (ctx: ExtensionContext): void => {
99
+ if (showCacheDisplay) updateCacheStatus(ctx, contextLoader);
100
+ else {
101
+ ctx.ui.setStatus(CACHE_STATUS_KEY, undefined);
102
+ ctx.ui.setWidget(CACHE_WIDGET_KEY, undefined);
103
+ }
104
+ };
105
+
106
+ pi.on("session_start", async (_event, ctx) => {
107
+ await Promise.resolve();
108
+ refreshCacheDisplay(ctx);
109
+ if (!ctx.hasUI) return;
110
+ const modelLabel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none";
111
+ const state = contextLoader.isEnabled(ctx) ? "on" : "off";
112
+ const inherited = inheritance.status(ctx);
113
+ const inheritanceLabel = inherited.state === "admitted" ? `admitted generation ${inherited.capsule.generation}` : inherited.state;
114
+ const inheritanceNotice = inheritanceLabel;
115
+ const messages = committedMessages(ctx);
116
+ if (messages > 0) {
117
+ ctx.ui.notify(`Session resumed (${messages} messages) · DSH context ${state} · inheritance ${inheritanceNotice} · Prime loop · model ${modelLabel}`, inherited.state === "degraded" || inherited.state === "incompatible" ? "warning" : "info");
118
+ return;
119
+ }
120
+ const sibling = recentSiblingSession(ctx);
121
+ const hint = sibling ? ` · a session from ${sibling.ageMinutes} min ago exists — resume it to keep context` : "";
122
+ ctx.ui.notify(`Fresh session · DSH context ${state} · inheritance ${inheritanceNotice} · Prime loop · model ${modelLabel}${hint}`, inherited.state === "degraded" || inherited.state === "incompatible" ? "warning" : "info");
123
+ });
124
+
125
+ pi.on("context", (_event, ctx) => { refreshCacheDisplay(ctx); });
126
+ pi.on("message_end", (event, ctx) => {
127
+ const latest = contextLoader.observeFinalizedAssistant(ctx, event.message);
128
+ if (latest) refreshCacheDisplay(ctx);
129
+ });
130
+ // Re-emit native status when a daemon UI can newly attach or replace its model.
131
+ pi.on("model_select", (_event, ctx) => { refreshCacheDisplay(ctx); });
132
+ pi.on("session_info_changed", (_event, ctx) => { refreshCacheDisplay(ctx); });
133
+ pi.on("session_shutdown", async (_event, ctx) => {
134
+ await Promise.resolve();
135
+ ctx?.ui?.setStatus?.(CACHE_STATUS_KEY, undefined);
136
+ ctx?.ui?.setWidget?.(CACHE_WIDGET_KEY, undefined);
137
+ });
138
+
139
+ pi.registerCommand("dsh", {
140
+ description: "Toggle DSH cache-rate text and report the resulting state",
141
+ handler: async (args, ctx) => {
142
+ await Promise.resolve();
143
+ const action = args.trim().toLowerCase();
144
+ if (action === "") showCacheDisplay = !showCacheDisplay;
145
+ else if (action === "on") showCacheDisplay = true;
146
+ else if (action === "off") showCacheDisplay = false;
147
+ else {
148
+ ctx.ui.notify("Usage: /dsh [on|off]", "warning");
149
+ return;
150
+ }
151
+ refreshCacheDisplay(ctx);
152
+ const scope = contextLoader.status(ctx);
153
+ const rates = cacheFooterText(scope?.latestCache?.efficiency, canonicalSessionEfficiency(scope)).replace(/^DSH cache · /u, "");
154
+ ctx.ui.notify(
155
+ `DSH ${DSH_VERSION} · cache text ${showCacheDisplay ? "ON" : "OFF"} · indexing ${contextLoader.isEnabled(ctx) ? "ACTIVE" : "PAUSED"} · ${rates}`,
156
+ scope?.lastError ? "warning" : "info",
157
+ );
158
+ },
159
+ });
160
+
161
+ }
@@ -0,0 +1,168 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import type { Message } from "@deepseek-ai/dsh-llm";
4
+ import { ShadowContextTelemetry, type ShadowLocation, type ShadowTraceEntry } from "../src/shadow-telemetry.js";
5
+ import { ContextService } from "../src/dsh-context-service.js";
6
+ import { ContextProtocolClient, type BranchKey } from "../src/context-protocol.js";
7
+ import { primeToDshAsync, dshToPrimeAsync, type PrimeEnvelope, type PrimeMessage } from "../src/context-converter.js";
8
+ import { stableJson } from "../src/prefix-metrics.js";
9
+ import { LocalDshImageAttachments, type DshImageAttachmentGateway } from "../src/dsh-image-attachments.js";
10
+
11
+ interface MirrorCounters {
12
+ syncs: number;
13
+ skips: number;
14
+ errors: number;
15
+ appends: number;
16
+ noops: number;
17
+ rebuilds: number;
18
+ }
19
+ interface SyncResult { revision: number; messageCount: number; mode: "append" | "noop" | "rebuild"; }
20
+ interface ProjectResult { messages: Message[]; }
21
+ export type ShadowMode = "off" | "on";
22
+ export interface ShadowContextOptions { mode?: ShadowMode; maxMessages?: number; maxBytes?: number; }
23
+ export function shadowOptions(env: NodeJS.ProcessEnv = process.env): Required<ShadowContextOptions> {
24
+ const rawMode = env.PRIME_DSH_SHADOW_MODE ?? "off";
25
+ if (rawMode !== "off" && rawMode !== "on") throw new Error("PRIME_DSH_SHADOW_MODE must be off or on");
26
+ const positive = (value: string | undefined, fallback: number): number => {
27
+ const parsed = value === undefined ? fallback : Number(value);
28
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
29
+ };
30
+ return { mode: rawMode, maxMessages: positive(env.PRIME_DSH_SHADOW_MAX_MESSAGES, 500), maxBytes: positive(env.PRIME_DSH_SHADOW_MAX_BYTES, 4 * 1024 * 1024) };
31
+ }
32
+
33
+ type JsonObject = Record<string, unknown>;
34
+ const isObject = (value: unknown): value is JsonObject => typeof value === "object" && value !== null && !Array.isArray(value);
35
+ function asPrimeInput(value: unknown): PrimeMessage | PrimeEnvelope {
36
+ if (!isObject(value)) throw new TypeError("Prime context message must be an object");
37
+ if (isObject(value.message) && typeof value.message.role === "string") return value as PrimeEnvelope;
38
+ if (typeof value.role === "string") return value as PrimeMessage;
39
+ throw new TypeError("Prime context message has no role");
40
+ }
41
+ function isSyncResult(value: unknown): value is SyncResult {
42
+ return isObject(value) && typeof value.revision === "number" && typeof value.messageCount === "number"
43
+ && (value.mode === "append" || value.mode === "noop" || value.mode === "rebuild");
44
+ }
45
+ function isProjectResult(value: unknown): value is ProjectResult {
46
+ return isObject(value) && Array.isArray(value.messages);
47
+ }
48
+
49
+ function location(ctx: ExtensionContext): ShadowLocation {
50
+ return { sessionId: ctx.sessionManager.getSessionId(), branchId: ctx.sessionManager.getLeafId() ?? "root" };
51
+ }
52
+ function metric(entry: ShadowTraceEntry | undefined): string {
53
+ if (!entry) return "none";
54
+ return `#${entry.request} ${entry.bytes}B sha256:${entry.digest.slice(0, 12)} lcp=${entry.commonPrefixBytes}B (${(entry.prefixRatio * 100).toFixed(1)}%) ${entry.reason}`;
55
+ }
56
+
57
+ /** Owns the fail-open DSH mirror lifecycle and typed protocol boundary. */
58
+ export class ShadowMirrorController {
59
+ private readonly service: ContextService;
60
+ private readonly client: ContextProtocolClient;
61
+ private readonly revisions = new Map<string, number>();
62
+ readonly counters: MirrorCounters = { syncs: 0, skips: 0, errors: 0, appends: 0, noops: 0, rebuilds: 0 };
63
+
64
+ constructor(service = new ContextService(), private readonly attachments: DshImageAttachmentGateway = new LocalDshImageAttachments()) {
65
+ this.service = service;
66
+ this.client = new ContextProtocolClient(request => this.service.handle(request));
67
+ const initialized = this.client.call("initialize");
68
+ if (!initialized.ok) throw new Error(`Could not initialize context shadow: ${initialized.error.message}`);
69
+ }
70
+
71
+ async observe(messages: readonly unknown[], key: BranchKey): Promise<void> {
72
+ const encodedKey = JSON.stringify([key.sessionId, key.branchId]);
73
+ try {
74
+ const canonical = await Promise.all(messages.map((message, index) => primeToDshAsync(asPrimeInput(message), { admitImages: (images) => this.attachments.admitPrimeImages(images) }, this.messageId(message, index))));
75
+ const synced = this.client.call("session/sync-canonical", { key, messages: canonical, expectedRevision: this.revisions.get(encodedKey) ?? 0 });
76
+ if (!synced.ok || !isSyncResult(synced.result)) { this.counters.errors++; return; }
77
+ const projected = this.client.call("project", { key });
78
+ if (!projected.ok || !isProjectResult(projected.result)) { this.counters.errors++; return; }
79
+ const roundTrip = await Promise.all(projected.result.messages.map((message) => dshToPrimeAsync(message, { resolveImage: (attachment) => this.attachments.resolveDshImage(attachment) })));
80
+ if (stableJson(roundTrip) !== stableJson(messages)) { this.counters.skips++; return; }
81
+ this.revisions.set(encodedKey, synced.result.revision);
82
+ this.counters.syncs++;
83
+ if (synced.result.mode === "append") this.counters.appends++;
84
+ else if (synced.result.mode === "noop") this.counters.noops++;
85
+ else this.counters.rebuilds++;
86
+ } catch { this.counters.errors++; }
87
+ }
88
+
89
+ private messageId(message: unknown, index: number): string {
90
+ return `prime-${createHash("sha256").update(`${index}:`).update(stableJson(message)).digest("hex").slice(0, 32)}`;
91
+ }
92
+ }
93
+
94
+ /** Registers passive observers and owns their command-facing presentation. */
95
+ export class ShadowContextExtension {
96
+ readonly telemetry: ShadowContextTelemetry;
97
+ readonly mirror: ShadowMirrorController | undefined;
98
+
99
+ private readonly options: Required<ShadowContextOptions>;
100
+ private boundedSkips = 0;
101
+ constructor(private readonly pi: ExtensionAPI, telemetry = new ShadowContextTelemetry(), mirror?: ShadowMirrorController, options: ShadowContextOptions = shadowOptions()) {
102
+ this.telemetry = telemetry;
103
+ this.options = { mode: options.mode ?? "off", maxMessages: options.maxMessages ?? 500, maxBytes: options.maxBytes ?? 4 * 1024 * 1024 };
104
+ this.mirror = this.options.mode === "on" ? (mirror ?? new ShadowMirrorController()) : undefined;
105
+ }
106
+
107
+ register(registerCommands = true): ShadowContextTelemetry {
108
+ if (this.options.mode === "on") {
109
+ this.pi.on("context", (event, ctx) => {
110
+ const here = location(ctx);
111
+ if (event.messages.length > this.options.maxMessages || Buffer.byteLength(stableJson(event.messages)) > this.options.maxBytes) { this.boundedSkips++; return; }
112
+ this.telemetry.observe("context", event.messages, here);
113
+ if (this.mirror) void this.mirror.observe(event.messages, here);
114
+ });
115
+ this.pi.on("before_provider_request", (event, ctx) => {
116
+ this.telemetry.observe("before_provider_request", event.payload, location(ctx));
117
+ });
118
+ }
119
+ if (registerCommands) {
120
+ this.registerStatusCommand();
121
+ this.registerTraceCommand();
122
+ }
123
+ return this.telemetry;
124
+ }
125
+
126
+ notifyStatus(ctx: ExtensionContext): void {
127
+ const here = location(ctx);
128
+ if (this.options.mode === "off") { ctx.ui.notify("DSH context shadow is off (normal Prime path). Set PRIME_DSH_SHADOW_MODE=on and reload to enable diagnostics.", "info"); return; }
129
+ const status = this.telemetry.status(here.sessionId, here.branchId) ?? this.telemetry.status(here.sessionId);
130
+ if (!status) { ctx.ui.notify(`DSH context shadow: no observations for session ${here.sessionId}`, "info"); return; }
131
+ const c = this.mirror?.counters ?? { syncs: 0, skips: 0, errors: 0, appends: 0, noops: 0, rebuilds: 0 };
132
+ ctx.ui.notify(`DSH context shadow session=${status.sessionId} branch=${status.branchId} observations=${status.observations} errors=${status.errors}
133
+ context ${metric(status.context)}
134
+ provider ${metric(status.provider)}
135
+ DSH mode=${this.options.mode} bounded-skips=${this.boundedSkips}
136
+ DSH mirror syncs=${c.syncs} append=${c.appends} noop=${c.noops} rebuild=${c.rebuilds} skips=${c.skips} errors=${c.errors}`, status.errors || c.errors ? "warning" : "info");
137
+ }
138
+
139
+ private registerStatusCommand(): void {
140
+ this.pi.registerCommand("dsh-context-status", {
141
+ description: "Show passive Prime context/provider prefix telemetry",
142
+ handler: async (_args, ctx) => { await Promise.resolve(); this.notifyStatus(ctx); },
143
+ });
144
+ }
145
+
146
+ notifyTrace(args: string, ctx: ExtensionContext): void {
147
+ const here = location(ctx);
148
+ const arg = args.trim().toLowerCase();
149
+ if (arg === "clear") { this.telemetry.clear(here.sessionId); ctx.ui.notify("DSH context shadow trace cleared for this session", "info"); return; }
150
+ const requested = arg ? Number.parseInt(arg, 10) : 10;
151
+ const count = Number.isFinite(requested) ? Math.min(Math.max(requested, 1), 50) : 10;
152
+ const entries = this.telemetry.traces(here.sessionId, count);
153
+ const text = entries.length ? entries.map((entry) => `${entry.observedAt} branch=${entry.branchId} ${entry.stage} ${metric(entry)}`).join("\n") : "No context shadow trace observations.";
154
+ ctx.ui.notify(text, "info");
155
+ }
156
+
157
+ private registerTraceCommand(): void {
158
+ this.pi.registerCommand("dsh-context-trace", {
159
+ description: "Show or clear passive context fingerprint trace (/dsh-context-trace [count|clear])",
160
+ handler: async (args, ctx) => { await Promise.resolve(); this.notifyTrace(args, ctx); },
161
+ });
162
+ }
163
+ }
164
+
165
+ /** Compatibility entry point used by the package extension. */
166
+ export function registerShadowContextTelemetry(pi: ExtensionAPI): ShadowContextTelemetry {
167
+ return new ShadowContextExtension(pi).register();
168
+ }
package/package.json ADDED
@@ -0,0 +1,109 @@
1
+ {
2
+ "name": "prime-agent-dsh",
3
+ "version": "0.2.0",
4
+ "description": "DeepSeek Harness context sidecar and Python context objects for Prime Agent.",
5
+ "keywords": [
6
+ "prime-agent",
7
+ "prime-agent-package",
8
+ "pi-package",
9
+ "context-management",
10
+ "context-window",
11
+ "deepseek-harness"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/moreWax/prime-agent-dsh.git"
16
+ },
17
+ "homepage": "https://github.com/moreWax/prime-agent-dsh#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/moreWax/prime-agent-dsh/issues"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "provenance": true
24
+ },
25
+ "type": "module",
26
+ "license": "MIT",
27
+ "files": [
28
+ "extensions/index.ts",
29
+ "extensions/shadow-context.ts",
30
+ "src/context-converter.ts",
31
+ "src/context-objects.ts",
32
+ "src/context-protocol.ts",
33
+ "src/durable-context-store.ts",
34
+ "src/dsh-context-service.ts",
35
+ "src/dsh-image-attachments.ts",
36
+ "src/prefix-metrics.ts",
37
+ "src/recursive-context-loader.ts",
38
+ "src/rlm-context-bootstrap.ts",
39
+ "src/rlm-context-inheritance.ts",
40
+ "src/shadow-telemetry.ts",
41
+ "src/context-spill.ts",
42
+ "src/durable-context-query.ts",
43
+ "src/durable-file-attachments.ts",
44
+ "src/provider-cache-series.ts",
45
+ "skills/dsh-context/SKILL.md",
46
+ "skills/dsh-context/pyproject.toml",
47
+ "skills/dsh-context/src/dsh_context/__init__.py",
48
+ "docs/shadow-telemetry-validation.md",
49
+ "scripts/patch-pi-ai-partial-json.mjs",
50
+ "docs/context-spill.md",
51
+ "docs/durable-context-query.md",
52
+ "docs/single-window-cache-architecture.md",
53
+ "scripts/package-smoke.mjs",
54
+ "README.md",
55
+ "LICENSE",
56
+ "THIRD_PARTY_NOTICES.md",
57
+ "CHANGELOG.md",
58
+ "SECURITY.md",
59
+ "docs/getting-started.md",
60
+ "docs/security.md"
61
+ ],
62
+ "pi": {
63
+ "extensions": [
64
+ "./extensions/index.ts"
65
+ ],
66
+ "skills": [
67
+ "./skills"
68
+ ]
69
+ },
70
+ "scripts": {
71
+ "typecheck": "tsc --noEmit",
72
+ "test": "node --import tsx --test tests/*.test.ts",
73
+ "test:python": "PYTHONPATH=skills/dsh-context/src python3 -m unittest discover -s tests/python -p 'test_*.py'",
74
+ "check": "npm run typecheck && npm run lint && npm test && npm run test:python",
75
+ "lint": "eslint \"src/**/*.ts\" \"extensions/**/*.ts\" \"tests/**/*.ts\"",
76
+ "package:smoke": "node scripts/package-smoke.mjs",
77
+ "release:check": "npm run check && npm run package:smoke",
78
+ "prepublishOnly": "npm run release:check",
79
+ "postinstall": "node scripts/patch-pi-ai-partial-json.mjs"
80
+ },
81
+ "dependencies": {
82
+ "@deepseek-ai/cordis": "4.0.2",
83
+ "@deepseek-ai/dsh-attachment": "0.1.6-alpha.2",
84
+ "@deepseek-ai/dsh-attachment-local": "0.1.6-alpha.2",
85
+ "@deepseek-ai/dsh-llm": "0.1.6-alpha.2",
86
+ "@deepseek-ai/dsh-session": "0.1.6-alpha.2",
87
+ "partial-json": "0.1.7"
88
+ },
89
+ "peerDependencies": {
90
+ "@earendil-works/pi-ai": ">=0.86.1",
91
+ "@earendil-works/pi-coding-agent": ">=0.86.1",
92
+ "typebox": "*"
93
+ },
94
+ "devDependencies": {
95
+ "@eslint/js": "^10.0.1",
96
+ "@types/js-yaml": "^4.0.9",
97
+ "@types/node": "^22.0.0",
98
+ "eslint": "^10.9.1",
99
+ "tsx": "^4.20.0",
100
+ "typescript": "^5.9.0",
101
+ "typescript-eslint": "^8.69.0",
102
+ "@earendil-works/pi-ai": "0.86.1",
103
+ "@earendil-works/pi-coding-agent": "0.86.1",
104
+ "typebox": "1.3.34"
105
+ },
106
+ "engines": {
107
+ "node": "^22.19.0 || >=24.0.0"
108
+ }
109
+ }
@@ -0,0 +1,148 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+
8
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
9
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
10
+ const originalHome = process.env.HOME ?? process.env.USERPROFILE;
11
+ const repositoryUrl = "https://github.com/moreWax/prime-agent-dsh";
12
+ const expectedKeywords = [
13
+ "prime-agent", "prime-agent-package", "pi-package", "context-management", "context-window", "deepseek-harness",
14
+ ];
15
+ const communityFiles = [
16
+ "CHANGELOG.md",
17
+ "CODE_OF_CONDUCT.md",
18
+ "CONTRIBUTING.md",
19
+ "ROADMAP.md",
20
+ "SECURITY.md",
21
+ "SUPPORT.md",
22
+ "docs/getting-started.md",
23
+ "docs/security.md",
24
+ ".github/dependabot.yml",
25
+ ".github/ISSUE_TEMPLATE/bug_report.yml",
26
+ ".github/ISSUE_TEMPLATE/config.yml",
27
+ ".github/ISSUE_TEMPLATE/feature_request.yml",
28
+ ".github/PULL_REQUEST_TEMPLATE.md",
29
+ ".github/workflows/ci.yml",
30
+ ".github/workflows/publish.yml",
31
+ ];
32
+ const expected = [
33
+ "CHANGELOG.md", "LICENSE", "README.md", "SECURITY.md", "THIRD_PARTY_NOTICES.md", "package.json",
34
+ "docs/context-spill.md", "docs/durable-context-query.md", "docs/getting-started.md", "docs/security.md", "docs/shadow-telemetry-validation.md", "docs/single-window-cache-architecture.md",
35
+ "extensions/index.ts", "extensions/shadow-context.ts", "scripts/package-smoke.mjs", "scripts/patch-pi-ai-partial-json.mjs",
36
+ "skills/dsh-context/SKILL.md", "skills/dsh-context/pyproject.toml", "skills/dsh-context/src/dsh_context/__init__.py",
37
+ "src/context-converter.ts", "src/context-objects.ts", "src/context-protocol.ts", "src/context-spill.ts",
38
+ "src/dsh-context-service.ts", "src/durable-context-query.ts", "src/durable-context-store.ts", "src/durable-file-attachments.ts", "src/dsh-image-attachments.ts", "src/prefix-metrics.ts", "src/provider-cache-series.ts", "src/recursive-context-loader.ts", "src/rlm-context-bootstrap.ts", "src/rlm-context-inheritance.ts", "src/shadow-telemetry.ts",
39
+ ].sort();
40
+
41
+ function run(command, args, options = {}) {
42
+ const result = spawnSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], ...options });
43
+ if (result.error) throw result.error;
44
+ if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} failed (${result.status})\n${result.stdout}${result.stderr}`);
45
+ return result.stdout;
46
+ }
47
+
48
+ for (const path of communityFiles) {
49
+ const contents = await readFile(join(root, path), "utf8");
50
+ assert(contents.trim().length > 0, `${path} must not be empty`);
51
+ }
52
+ const ciWorkflow = await readFile(join(root, ".github/workflows/ci.yml"), "utf8");
53
+ assert.match(ciWorkflow, /node: \[22, 24\]/, "CI must test all supported Node.js majors");
54
+ assert.match(ciWorkflow, /npm ci[\s\S]*npm run release:check/, "CI must validate the clean install");
55
+ const publishWorkflow = await readFile(join(root, ".github/workflows/publish.yml"), "utf8");
56
+ assert.match(publishWorkflow, /id-token: write/, "npm trusted publishing needs OIDC permission");
57
+ assert.match(publishWorkflow, /npm publish --access public --provenance/, "release publishing must include provenance");
58
+ assert(!/npm_[A-Za-z0-9]{20,}/.test(publishWorkflow), "publish workflow appears to contain an npm token");
59
+
60
+ const temp = await mkdtemp(join(tmpdir(), "prime-agent-dsh-pack-"));
61
+ try {
62
+ const home = join(temp, "home");
63
+ await (await import("node:fs/promises")).mkdir(home);
64
+ const isolatedEnv = { ...process.env, HOME: home, USERPROFILE: home, XDG_CONFIG_HOME: join(home, ".config") };
65
+ if (originalHome) isolatedEnv.npm_config_cache = join(originalHome, ".npm");
66
+ Object.assign(process.env, { HOME: home, USERPROFILE: home, XDG_CONFIG_HOME: join(home, ".config") });
67
+ const packOutput = run(npm, ["pack", "--json", "--ignore-scripts", "--pack-destination", temp], { cwd: root, env: isolatedEnv });
68
+ const pack = JSON.parse(packOutput)[0];
69
+ assert(pack?.filename, "npm pack did not report a tarball");
70
+ const actual = pack.files.map(({ path }) => path).sort();
71
+ assert.deepEqual(actual, expected, "packed artifact does not match the release allowlist");
72
+ assert(!actual.some((path) => /(^|\/)(test|tests|fixtures)(\/|$)|(?:^|\.)test\.[^/]+$/i.test(path)), "test material leaked into the tarball");
73
+ assert(!actual.some((path) => /(^|\/)(?:\.env(?:\.|$)|\.npmrc$|\.pypirc$|auth\.json$|credentials?(?:\.|\/|$)|id_rsa$)|\.(?:pem|key|p12)$/i.test(path)), "a secret-bearing filename leaked into the tarball");
74
+ assert(!actual.some((path) => path.startsWith(".github/")), "repository community files leaked into the runtime package");
75
+ assert(!actual.some((path) => /(?:dsh-provider|model-wrapper|agent-pool|branch-checkpoint|acp-client|transparent-routing)/i.test(path)), "legacy model/agent-loop source leaked into the tarball");
76
+
77
+ const project = join(temp, "consumer");
78
+ const { mkdir } = await import("node:fs/promises");
79
+ await mkdir(project);
80
+ run(npm, ["init", "--yes"], { cwd: project, env: isolatedEnv });
81
+ const tarball = join(temp, pack.filename);
82
+ run(npm, ["install", "--omit=dev", tarball,
83
+ "@earendil-works/pi-coding-agent@0.86.1", "@earendil-works/pi-ai@0.86.1", "typebox@1.3.34"], { cwd: project, env: isolatedEnv });
84
+
85
+ const installed = join(project, "node_modules", "prime-agent-dsh");
86
+ const manifest = JSON.parse(await readFile(join(installed, "package.json"), "utf8"));
87
+ assert.equal(manifest.version, "0.2.0", "packed plugin version is stale");
88
+ assert.deepEqual(manifest.repository, { type: "git", url: `git+${repositoryUrl}.git` });
89
+ assert.equal(manifest.homepage, `${repositoryUrl}#readme`);
90
+ assert.deepEqual(manifest.bugs, { url: `${repositoryUrl}/issues` });
91
+ assert.deepEqual(manifest.publishConfig, { access: "public", provenance: true });
92
+ assert.deepEqual(manifest.keywords, expectedKeywords);
93
+ assert.equal(manifest.funding, undefined, "do not advertise a funding destination that the project does not provide");
94
+ assert.equal(manifest.peerDependencies["@earendil-works/pi-coding-agent"], ">=0.86.1");
95
+ assert.deepEqual(manifest.pi, { extensions: ["./extensions/index.ts"], skills: ["./skills"] });
96
+ const packedReadme = await readFile(join(installed, "README.md"), "utf8");
97
+ assert.match(packedReadme, /Getting started/);
98
+ const packedGettingStarted = await readFile(join(installed, "docs", "getting-started.md"), "utf8");
99
+ assert.match(packedGettingStarted, /Agents[\s\S]*Ctrl\+X[^\n]*twice/);
100
+ assert.match(packedGettingStarted, /Prime deletes the matching session artifact directory/);
101
+ assert.deepEqual(
102
+ Object.keys(manifest.dependencies).filter((name) => name.startsWith("@deepseek-ai/")).sort(),
103
+ ["@deepseek-ai/cordis", "@deepseek-ai/dsh-attachment", "@deepseek-ai/dsh-attachment-local", "@deepseek-ai/dsh-llm", "@deepseek-ai/dsh-session"],
104
+ "production package must depend only on sidecar DSH components",
105
+ );
106
+ const piAiCandidates = [
107
+ join(installed, "node_modules", "@earendil-works", "pi-ai", "dist", "utils", "json-parse.js"),
108
+ join(project, "node_modules", "@earendil-works", "pi-ai", "dist", "utils", "json-parse.js"),
109
+ ];
110
+ let patchedPiAi;
111
+ for (const candidate of piAiCandidates) {
112
+ try { patchedPiAi = await readFile(candidate, "utf8"); break; } catch { /* try npm's other legal placement */ }
113
+ }
114
+ assert.match(patchedPiAi ?? "", /\.\.\/\.\.\/\.\.\/\.\.\/partial-json\/dist\/index\.js/, "Bun partial-json compatibility patch was not applied");
115
+
116
+ const host = await import(pathToFileURL(join(project, "node_modules", "@earendil-works", "pi-coding-agent", "dist", "index.js")));
117
+ const settingsManager = host.SettingsManager.inMemory({ packages: [installed] }, { projectTrusted: true });
118
+ const loader = new host.DefaultResourceLoader({ cwd: project, agentDir: join(temp, "home", ".prime", "agent"), settingsManager, noContextFiles: true });
119
+ await loader.reload();
120
+ let extensions = loader.getExtensions();
121
+ let skills = loader.getSkills();
122
+ assert.equal(extensions.errors.length, 0, JSON.stringify(extensions.errors));
123
+ assert.equal(extensions.extensions.length, 1, "Prime did not discover exactly one package extension");
124
+ assert.equal(extensions.extensions[0].tools.size, 0, "context-sidecar extension must not replace Prime tools");
125
+ assert.equal((extensions.extensions[0].handlers.get("before_agent_start") ?? []).length, 1, "task-aware inheritance admission handler is missing");
126
+ assert((extensions.extensions[0].handlers.get("context") ?? []).length >= 2, "inheritance ordering/context projection handlers are missing");
127
+ for (const event of ["message_end", "model_select", "session_info_changed", "session_shutdown"]) {
128
+ assert((extensions.extensions[0].handlers.get(event) ?? []).length > 0, `${event} native lifecycle handler is missing`);
129
+ }
130
+ assert.equal(extensions.extensions[0].messageRenderers.size, 0, "status must not use a custom message/footer renderer");
131
+ assert(!skills.skills.some(({ name }) => name === "deepseek-harness"), "legacy delegation skill must not ship without its removed tool");
132
+ assert(skills.skills.some(({ name }) => name === "dsh-context"), "Prime did not discover the dsh-context skill");
133
+ assert.equal(skills.diagnostics.length, 0, JSON.stringify(skills.diagnostics));
134
+ const shutdownHandlers = extensions.extensions[0].handlers.get("session_shutdown") ?? [];
135
+ assert(shutdownHandlers.length > 0, "extension did not register session_shutdown cleanup");
136
+ for (const handler of shutdownHandlers) await handler({}, {});
137
+
138
+ await loader.reload();
139
+ extensions = loader.getExtensions();
140
+ skills = loader.getSkills();
141
+ assert.equal(extensions.errors.length, 0, "extension reload produced an error");
142
+ assert.equal(extensions.extensions.length, 1, "extension was not retained across reload");
143
+ assert(!skills.skills.some(({ name }) => name === "deepseek-harness"), "legacy delegation skill returned after reload");
144
+ assert(skills.skills.some(({ name }) => name === "dsh-context"), "context skill was not retained across reload");
145
+ console.log(`package smoke passed: ${actual.length} files; production install; extension + context skill discovery; reload`);
146
+ } finally {
147
+ await rm(temp, { recursive: true, force: true });
148
+ }
@@ -0,0 +1,14 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-ai"));
6
+ const target = join(dirname(entry), "utils", "json-parse.js");
7
+ const bare = 'from "partial-json"';
8
+ const relative = 'from "../../../../partial-json/dist/index.js"';
9
+ const source = await readFile(target, "utf8");
10
+ if (source.includes(relative)) process.exit(0);
11
+ if (!source.includes(bare)) {
12
+ throw new Error(`Cannot apply Prime Bun compatibility patch: unexpected ${target}`);
13
+ }
14
+ await writeFile(target, source.replace(bare, relative));