@cortexkit/opencode-magic-context 0.36.1 → 0.37.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.
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +2 -1
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +10 -0
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-mt0ttc4x.js → conflict-warning-hook-an4fxhwf.js} +3 -3
- package/dist/{derive-budgets-88mxk2w1.js → derive-budgets-0f8xpyw2.js} +3 -2
- package/dist/features/magic-context/context-authority.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts +3 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity.d.ts +6 -0
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/runner.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/wake-plane.d.ts +21 -0
- package/dist/features/magic-context/smart-notes/wake-plane.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts +13 -2
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +22 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-primers.d.ts +11 -0
- package/dist/features/magic-context/storage-primers.d.ts.map +1 -1
- package/dist/features/magic-context/storage-schema-helpers.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +7 -0
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
- package/dist/features/magic-context/window-report-ledger.d.ts +70 -0
- package/dist/features/magic-context/window-report-ledger.d.ts.map +1 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts +4 -0
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts +2 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts +20 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-state-sync.d.ts +8 -0
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-wire.d.ts +6 -1
- package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +17 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +25 -0
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +11 -3
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +10 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
- package/dist/{index-1skzp4dc.js → index-0ze9p983.js} +572 -92
- package/dist/{index-nsx2n9eg.js → index-4cz5nfxf.js} +3 -3
- package/dist/{index-f1tnb3vm.js → index-8pqfcghm.js} +21 -9
- package/dist/{index-zp6x6s6a.js → index-cae89eas.js} +25 -6
- package/dist/index-cgyfn1s2.js +53 -0
- package/dist/{index-3zqcdwqr.js → index-crke4z5j.js} +7 -4
- package/dist/{index-tsgs7td8.js → index-pck9vadr.js} +1 -1
- package/dist/index-q0t189ay.js +741 -0
- package/dist/{index-3tnkymva.js → index-sw8gnq34.js} +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1172 -287
- package/dist/plugin/hooks/create-session-hooks.d.ts +3 -0
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-a0g9b52q.js → recomp-orchestrator-c3ff16k7.js} +7 -6
- package/dist/{safe-notification-target-c11fdda2.js → safe-notification-target-b693f9x3.js} +2 -2
- package/dist/{send-session-notification-bgtf4853.js → send-session-notification-dxske5zm.js} +3 -3
- package/dist/shared/conflict-detector.d.ts +63 -0
- package/dist/shared/conflict-detector.d.ts.map +1 -1
- package/dist/shared/harness-provider-map.d.ts +8 -0
- package/dist/shared/harness-provider-map.d.ts.map +1 -1
- package/dist/shared/logger.d.ts +8 -0
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/models-dev-cache.d.ts +5 -0
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
- package/dist/shared/prompt-surface.d.ts +2 -0
- package/dist/shared/prompt-surface.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +14 -0
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +19 -9
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/window-geometry.d.ts +89 -0
- package/dist/shared/window-geometry.d.ts.map +1 -0
- package/dist/{storage-meta-session-df9tstzr.js → storage-meta-session-mk4gsnke.js} +4 -3
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tools/ctx-note/tools.d.ts.map +1 -1
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/config/index.ts +2 -0
- package/src/config/latch-permanence-guard.test.ts +218 -0
- package/src/config/project-security.ts +10 -1
- package/src/config/schema/magic-context.ts +10 -0
- package/src/shared/__fixtures__/window-overlay.v1.json +337 -0
- package/src/shared/conflict-detector.test.ts +176 -1
- package/src/shared/conflict-detector.ts +126 -3
- package/src/shared/harness-provider-map.test.ts +20 -0
- package/src/shared/harness-provider-map.ts +19 -0
- package/src/shared/logger.test.ts +119 -0
- package/src/shared/logger.ts +33 -15
- package/src/shared/models-dev-cache.test.ts +17 -0
- package/src/shared/models-dev-cache.ts +58 -6
- package/src/shared/prompt-surface-runtime.ts +5 -3
- package/src/shared/prompt-surface.test.ts +38 -0
- package/src/shared/prompt-surface.ts +15 -2
- package/src/shared/rpc-client.ts +4 -2
- package/src/shared/rpc-types.ts +14 -0
- package/src/shared/rpc-utils.test.ts +137 -13
- package/src/shared/rpc-utils.ts +144 -31
- package/src/shared/window-geometry.test.ts +309 -0
- package/src/shared/window-geometry.ts +629 -0
- package/src/tui/data/context-db.ts +5 -0
- package/src/tui/index.tsx +22 -0
- package/src/tui-compiled/data/context-db.ts +5 -0
- package/src/tui-compiled/index.tsx +319 -255
- package/dist/index-wgvxkqpx.js +0 -342
|
@@ -30,9 +30,15 @@ import { join } from "node:path";
|
|
|
30
30
|
import type { ContextLimitProvenance } from "./context-limit-provenance";
|
|
31
31
|
import { getMagicContextStorageDir } from "./data-path";
|
|
32
32
|
import { getHarness } from "./harness";
|
|
33
|
-
import {
|
|
33
|
+
import { modelRefLookupOrder } from "./harness-provider-map";
|
|
34
34
|
import { sessionLog } from "./logger";
|
|
35
35
|
import { shouldEnforcePrivateStoragePermissions } from "./storage-permissions";
|
|
36
|
+
import {
|
|
37
|
+
deriveWindowGeometry,
|
|
38
|
+
getWindowOverlay,
|
|
39
|
+
resolveWindowOverlayFacts,
|
|
40
|
+
type WindowGeometryResult,
|
|
41
|
+
} from "./window-geometry";
|
|
36
42
|
|
|
37
43
|
interface OpencodeClientLike {
|
|
38
44
|
config: {
|
|
@@ -196,14 +202,11 @@ function isFinitePositive(value: number | undefined): value is number {
|
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
function modelKeyLookupOrder(providerID: string, modelID: string): string[] {
|
|
199
|
-
const
|
|
200
|
-
const canonicalFull = piModelRefToCanonical(full);
|
|
201
|
-
const candidates = [full, canonicalFull, modelID];
|
|
205
|
+
const candidates = [...modelRefLookupOrder(`${providerID}/${modelID}`), modelID];
|
|
202
206
|
const colon = modelID.lastIndexOf(":");
|
|
203
207
|
if (colon > 0) {
|
|
204
208
|
const bareModel = modelID.slice(0, colon);
|
|
205
|
-
|
|
206
|
-
candidates.push(providerBare, piModelRefToCanonical(providerBare), bareModel);
|
|
209
|
+
candidates.push(...modelRefLookupOrder(`${providerID}/${bareModel}`), bareModel);
|
|
207
210
|
}
|
|
208
211
|
return [...new Set(candidates)];
|
|
209
212
|
}
|
|
@@ -493,6 +496,50 @@ async function refreshModelLimitsOnce(client: OpencodeClientLike): Promise<boole
|
|
|
493
496
|
* `ctx.model.contextWindow`), so for Pi this returns `undefined` and Pi's
|
|
494
497
|
* own resolution path is used.
|
|
495
498
|
*/
|
|
499
|
+
export function getSdkWindowGeometry(
|
|
500
|
+
providerID: string,
|
|
501
|
+
modelID: string,
|
|
502
|
+
detectedContextLimit?: number,
|
|
503
|
+
options?: {
|
|
504
|
+
detectedLimitProvenance?: ContextLimitProvenance;
|
|
505
|
+
harness?: "opencode" | "pi";
|
|
506
|
+
},
|
|
507
|
+
): WindowGeometryResult | undefined {
|
|
508
|
+
loadPersistedApiCacheOnce();
|
|
509
|
+
const metadata = lookupMetadataWithTagFallback(apiCache, providerID, modelID);
|
|
510
|
+
if (!metadata) return undefined;
|
|
511
|
+
const rawContext = metadata.contextLimit ?? metadata.limit;
|
|
512
|
+
const promptOnlyDetected =
|
|
513
|
+
options?.detectedLimitProvenance === "prompt_only" && isFinitePositive(detectedContextLimit)
|
|
514
|
+
? detectedContextLimit
|
|
515
|
+
: undefined;
|
|
516
|
+
const result = deriveWindowGeometry(
|
|
517
|
+
providerID,
|
|
518
|
+
modelID,
|
|
519
|
+
{
|
|
520
|
+
context: rawContext,
|
|
521
|
+
input: metadata.inputLimit,
|
|
522
|
+
output: metadata.outputLimit,
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
overlay: resolveWindowOverlayFacts(providerID, modelID, getWindowOverlay()),
|
|
526
|
+
reserveConfig: outputReserveConfig,
|
|
527
|
+
harness: options?.harness ?? "opencode",
|
|
528
|
+
contextCap:
|
|
529
|
+
promptOnlyDetected === undefined && isFinitePositive(detectedContextLimit)
|
|
530
|
+
? detectedContextLimit
|
|
531
|
+
: undefined,
|
|
532
|
+
},
|
|
533
|
+
);
|
|
534
|
+
if (!result || promptOnlyDetected === undefined) return result;
|
|
535
|
+
const usableSoft = promptOnlyDetected;
|
|
536
|
+
return {
|
|
537
|
+
...result,
|
|
538
|
+
usableSoft,
|
|
539
|
+
usableHard: Math.max(usableSoft, Math.min(result.usableHard, promptOnlyDetected)),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
496
543
|
export function getSdkContextLimit(
|
|
497
544
|
providerID: string,
|
|
498
545
|
modelID: string,
|
|
@@ -502,6 +549,11 @@ export function getSdkContextLimit(
|
|
|
502
549
|
detectedLimitProvenance?: ContextLimitProvenance;
|
|
503
550
|
},
|
|
504
551
|
): number | undefined {
|
|
552
|
+
if (options?.reservation !== "none") {
|
|
553
|
+
return getSdkWindowGeometry(providerID, modelID, detectedContextLimit, {
|
|
554
|
+
detectedLimitProvenance: options?.detectedLimitProvenance,
|
|
555
|
+
})?.usableSoft;
|
|
556
|
+
}
|
|
505
557
|
loadPersistedApiCacheOnce();
|
|
506
558
|
const metadata = lookupMetadataWithTagFallback(apiCache, providerID, modelID);
|
|
507
559
|
if (!metadata) return undefined;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
CTX_REDUCE_LIGHT_DESCRIPTION,
|
|
14
14
|
CTX_SEARCH_LIGHT_DESCRIPTION,
|
|
15
15
|
} from "../tools/light-descriptions";
|
|
16
|
+
import { piModelRefToCanonical } from "./harness-provider-map";
|
|
16
17
|
import { detectConfigFile } from "./jsonc-parser";
|
|
17
18
|
import {
|
|
18
19
|
type PromptSurfaceConfig,
|
|
@@ -242,13 +243,14 @@ export function createPromptSurfaceGuidanceEpochCache(runtime: PromptSurfaceRunt
|
|
|
242
243
|
|
|
243
244
|
return {
|
|
244
245
|
resolve(sessionId, config, modelKey) {
|
|
246
|
+
const canonicalModelKey = modelKey ? piModelRefToCanonical(modelKey) : undefined;
|
|
245
247
|
const cached = epochs.get(sessionId);
|
|
246
|
-
if (cached && cached.config === config && cached.modelKey ===
|
|
248
|
+
if (cached && cached.config === config && cached.modelKey === canonicalModelKey) {
|
|
247
249
|
return cached.selection;
|
|
248
250
|
}
|
|
249
251
|
|
|
250
|
-
const selection = runtime.resolveGuidance(config,
|
|
251
|
-
epochs.set(sessionId, { config, modelKey, selection });
|
|
252
|
+
const selection = runtime.resolveGuidance(config, canonicalModelKey);
|
|
253
|
+
epochs.set(sessionId, { config, modelKey: canonicalModelKey, selection });
|
|
252
254
|
return selection;
|
|
253
255
|
},
|
|
254
256
|
clear(sessionId) {
|
|
@@ -47,6 +47,44 @@ describe("prompt-surface resolution", () => {
|
|
|
47
47
|
}
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
it("resolves Pi-native and canonical model keys with canonical precedence", () => {
|
|
51
|
+
const nativeKey = "openai-codex/gpt-5.6-sol";
|
|
52
|
+
const canonicalKey = "openai/gpt-5.6-sol";
|
|
53
|
+
|
|
54
|
+
expect(resolveCacheTtl({ default: "5m", [nativeKey]: "60m" }, canonicalKey)).toBe("60m");
|
|
55
|
+
expect(
|
|
56
|
+
resolvePromptSurface(
|
|
57
|
+
{ default: "full", models: { [nativeKey]: "light" } },
|
|
58
|
+
canonicalKey,
|
|
59
|
+
),
|
|
60
|
+
).toEqual({ preset: "light", source: "exact" });
|
|
61
|
+
expect(
|
|
62
|
+
resolveCacheTtl(
|
|
63
|
+
{ default: "5m", [nativeKey]: "60m", [canonicalKey]: "30m" },
|
|
64
|
+
nativeKey,
|
|
65
|
+
),
|
|
66
|
+
).toBe("30m");
|
|
67
|
+
expect(
|
|
68
|
+
resolvePromptSurface(
|
|
69
|
+
{ default: "full", models: { [nativeKey]: "light", [canonicalKey]: "full" } },
|
|
70
|
+
nativeKey,
|
|
71
|
+
),
|
|
72
|
+
).toEqual({ preset: "full", source: "exact" });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("leaves unknown provider keys unchanged", () => {
|
|
76
|
+
expect(
|
|
77
|
+
resolveCacheTtl(
|
|
78
|
+
{ default: "5m", "custom-provider/model": "1m" },
|
|
79
|
+
"custom-provider/model",
|
|
80
|
+
),
|
|
81
|
+
).toBe("1m");
|
|
82
|
+
expect(modelKeyLookupOrder("custom-provider/model")[0]).toEqual({
|
|
83
|
+
key: "custom-provider/model",
|
|
84
|
+
source: "exact",
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
50
88
|
it("derives a stable config identity independent of object key order", () => {
|
|
51
89
|
const left = promptSurfaceConfigIdentity({
|
|
52
90
|
default: "full",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { modelRefLookupOrder } from "./harness-provider-map";
|
|
2
|
+
|
|
1
3
|
/** The built-in prompt-surface variants. */
|
|
2
4
|
export type PromptSurfacePreset = "full" | "light";
|
|
3
5
|
|
|
@@ -69,6 +71,8 @@ export interface ModelKeyCandidate {
|
|
|
69
71
|
* provider/model boundary is the first slash; the rest of the string remains
|
|
70
72
|
* the model ID, including additional slashes. Candidates are case-sensitive.
|
|
71
73
|
*
|
|
74
|
+
* Known harness provider aliases are checked canonical-first at each specificity,
|
|
75
|
+
* so one shared config works on every harness and canonical wins on collisions.
|
|
72
76
|
* Provider wildcards are checked after progressively less-specific model keys,
|
|
73
77
|
* but before the caller's default. That keeps an exact or base-model override
|
|
74
78
|
* authoritative while still allowing `provider/*` to cover otherwise-unlisted
|
|
@@ -82,10 +86,15 @@ export function modelKeyLookupOrder(modelKey: string | undefined): ModelKeyCandi
|
|
|
82
86
|
|
|
83
87
|
const provider = modelKey.slice(0, slash);
|
|
84
88
|
let modelID = modelKey.slice(slash + 1);
|
|
89
|
+
const providerRefs = modelRefLookupOrder(`${provider}/${modelID}`);
|
|
85
90
|
const candidates: ModelKeyCandidate[] = [];
|
|
86
91
|
|
|
87
92
|
while (modelID.length > 0) {
|
|
88
|
-
|
|
93
|
+
for (const providerRef of providerRefs) {
|
|
94
|
+
const providerSlash = providerRef.indexOf("/");
|
|
95
|
+
const providerPrefix = providerRef.slice(0, providerSlash);
|
|
96
|
+
candidates.push({ key: `${providerPrefix}/${modelID}`, source: "exact" });
|
|
97
|
+
}
|
|
89
98
|
candidates.push({ key: modelID, source: "bare" });
|
|
90
99
|
|
|
91
100
|
const lastDash = modelID.lastIndexOf("-");
|
|
@@ -93,7 +102,11 @@ export function modelKeyLookupOrder(modelKey: string | undefined): ModelKeyCandi
|
|
|
93
102
|
modelID = modelID.slice(0, lastDash);
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
|
|
105
|
+
for (const providerRef of providerRefs) {
|
|
106
|
+
const providerSlash = providerRef.indexOf("/");
|
|
107
|
+
const providerPrefix = providerRef.slice(0, providerSlash);
|
|
108
|
+
candidates.push({ key: `${providerPrefix}/*`, source: "wildcard" });
|
|
109
|
+
}
|
|
97
110
|
|
|
98
111
|
const seen = new Set<string>();
|
|
99
112
|
return candidates.filter((candidate) => {
|
package/src/shared/rpc-client.ts
CHANGED
|
@@ -149,7 +149,9 @@ export class MagicContextRpcClient {
|
|
|
149
149
|
for (const entry of readdirSync(this.portDir)) {
|
|
150
150
|
if (!entry.startsWith("port-") || !entry.endsWith(".json")) continue;
|
|
151
151
|
const record = parseRpcPortFile(readFileSync(join(this.portDir, entry), "utf-8"));
|
|
152
|
-
|
|
152
|
+
// A denied liveness probe still leaves this as a candidate; the
|
|
153
|
+
// mandatory health check below confirms whether its RPC server is reachable.
|
|
154
|
+
if (!record || isPidAlive(record.pid) === "dead") continue;
|
|
153
155
|
records.push(record);
|
|
154
156
|
}
|
|
155
157
|
} catch {
|
|
@@ -158,7 +160,7 @@ export class MagicContextRpcClient {
|
|
|
158
160
|
|
|
159
161
|
try {
|
|
160
162
|
const legacy = parseRpcPortFile(readFileSync(this.legacyPortFilePath, "utf-8"));
|
|
161
|
-
if (legacy && (!legacy.pid || isPidAlive(legacy.pid))) records.push(legacy);
|
|
163
|
+
if (legacy && (!legacy.pid || isPidAlive(legacy.pid) !== "dead")) records.push(legacy);
|
|
162
164
|
} catch {
|
|
163
165
|
// Legacy discovery is optional.
|
|
164
166
|
}
|
package/src/shared/rpc-types.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
DreamTaskBacklogMap,
|
|
8
8
|
DreamTaskProgress,
|
|
9
9
|
} from "../features/magic-context/dreamer/task-registry";
|
|
10
|
+
import type { LoggerDiagnostics } from "./logger";
|
|
10
11
|
|
|
11
12
|
export interface SidebarSnapshot {
|
|
12
13
|
sessionId: string;
|
|
@@ -135,6 +136,17 @@ export interface StatusDetail extends SidebarSnapshot {
|
|
|
135
136
|
isSubagent: boolean;
|
|
136
137
|
pendingOps: Array<{ tagId: number; operation: string }>;
|
|
137
138
|
contextLimit: number;
|
|
139
|
+
windowGeometry?: {
|
|
140
|
+
usableSoft: number;
|
|
141
|
+
usableHard: number;
|
|
142
|
+
geometry: "shared_upfront" | "shared_truncating" | "separate";
|
|
143
|
+
derivation: {
|
|
144
|
+
window: number;
|
|
145
|
+
reserve: number;
|
|
146
|
+
reserveSource: "output_catalog" | "output_config" | "wall_margin" | "none";
|
|
147
|
+
geometry: "shared_upfront" | "shared_truncating" | "separate";
|
|
148
|
+
};
|
|
149
|
+
};
|
|
138
150
|
/**
|
|
139
151
|
* Parsed cache TTL in ms. -1 = never expires (cacheTtl "never"; Infinity
|
|
140
152
|
* cannot ride JSON-RPC and 0 would be indistinguishable from unset). The
|
|
@@ -170,6 +182,8 @@ export interface StatusDetail extends SidebarSnapshot {
|
|
|
170
182
|
toastDurationMs: number;
|
|
171
183
|
/** One-line status data for the experimental memory mural. */
|
|
172
184
|
mural?: { present: boolean; ageMs: number | null };
|
|
185
|
+
/** Runtime logger write failures observed by this plugin process. */
|
|
186
|
+
loggerDiagnostics: LoggerDiagnostics;
|
|
173
187
|
/**
|
|
174
188
|
* Stable storage-version probe: "which schema is the DB at, which fence does
|
|
175
189
|
* this binary carry". Field names are deliberately snake_case, mirroring the
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
__setRpcIdentityTestHooks,
|
|
9
9
|
discoverLivePiProcessIds,
|
|
10
10
|
inspectLivePiProcesses,
|
|
11
|
+
isPidAlive,
|
|
11
12
|
isPidIdentityPlausible,
|
|
12
13
|
type RpcPortFileRecord,
|
|
13
14
|
} from "./rpc-utils";
|
|
@@ -42,6 +43,13 @@ function psOutput(output: string | Error): typeof execFileSync {
|
|
|
42
43
|
}) as typeof execFileSync;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function tasklistOutput(entries: Array<[number, string]>): string {
|
|
47
|
+
return [
|
|
48
|
+
'"Image Name","PID","Session Name","Session#","Mem Usage"',
|
|
49
|
+
...entries.map(([pid, command]) => `"${command}","${pid}","Console","1","10,000 K"`),
|
|
50
|
+
].join("\r\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
afterEach(() => {
|
|
46
54
|
__resetRpcIdentityTestHooks();
|
|
47
55
|
});
|
|
@@ -78,6 +86,103 @@ describe("discoverLivePiProcessIds", () => {
|
|
|
78
86
|
error: "ps unavailable",
|
|
79
87
|
});
|
|
80
88
|
});
|
|
89
|
+
|
|
90
|
+
test("uses tasklist instead of ps on Windows", () => {
|
|
91
|
+
const calls: string[] = [];
|
|
92
|
+
__setRpcIdentityTestHooks({
|
|
93
|
+
platform: "win32",
|
|
94
|
+
processListExecFileSync: ((file: string | URL) => {
|
|
95
|
+
calls.push(String(file));
|
|
96
|
+
return tasklistOutput([
|
|
97
|
+
[process.pid, "pi.exe"],
|
|
98
|
+
[41001, "pi.exe"],
|
|
99
|
+
[41002, "opencode.exe"],
|
|
100
|
+
]);
|
|
101
|
+
}) as typeof execFileSync,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(inspectLivePiProcesses()).toEqual({
|
|
105
|
+
state: "known",
|
|
106
|
+
processIds: [41001],
|
|
107
|
+
});
|
|
108
|
+
expect(calls).toEqual(["tasklist"]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("isPidAlive", () => {
|
|
113
|
+
test("distinguishes confirmed, dead, and inaccessible PID probes", () => {
|
|
114
|
+
const probeFailure = (code: string): NodeJS.ErrnoException => {
|
|
115
|
+
const error = new Error(`kill failed: ${code}`) as NodeJS.ErrnoException;
|
|
116
|
+
error.code = code;
|
|
117
|
+
return error;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
__setRpcIdentityTestHooks({
|
|
121
|
+
processKill: (() => true) as typeof process.kill,
|
|
122
|
+
});
|
|
123
|
+
expect(isPidAlive(PID)).toBe("alive");
|
|
124
|
+
|
|
125
|
+
__setRpcIdentityTestHooks({
|
|
126
|
+
processKill: (() => {
|
|
127
|
+
throw probeFailure("ESRCH");
|
|
128
|
+
}) as typeof process.kill,
|
|
129
|
+
});
|
|
130
|
+
expect(isPidAlive(PID)).toBe("dead");
|
|
131
|
+
|
|
132
|
+
__setRpcIdentityTestHooks({
|
|
133
|
+
processKill: (() => {
|
|
134
|
+
throw probeFailure("EPERM");
|
|
135
|
+
}) as typeof process.kill,
|
|
136
|
+
});
|
|
137
|
+
expect(isPidAlive(PID)).toBe("inconclusive");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("uses tasklist for Windows liveness and captures probe stderr", () => {
|
|
141
|
+
const calls: Array<{ file: string; args: readonly string[]; stdio: unknown }> = [];
|
|
142
|
+
__setRpcIdentityTestHooks({
|
|
143
|
+
platform: "win32",
|
|
144
|
+
execFileSync: ((
|
|
145
|
+
file: string | URL,
|
|
146
|
+
args: readonly string[] = [],
|
|
147
|
+
options: { stdio?: unknown } = {},
|
|
148
|
+
) => {
|
|
149
|
+
calls.push({ file: String(file), args, stdio: options.stdio });
|
|
150
|
+
if (String(file) === "ps") throw new Error("ps must not run on Windows");
|
|
151
|
+
return tasklistOutput([[PID, "OpenCode.exe"]]);
|
|
152
|
+
}) as typeof execFileSync,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(isPidAlive(PID)).toBe("alive");
|
|
156
|
+
expect(calls).toEqual([
|
|
157
|
+
{
|
|
158
|
+
file: "tasklist",
|
|
159
|
+
args: ["/FO", "CSV", "/FI", `PID eq ${PID}`],
|
|
160
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
161
|
+
},
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("treats a successful tasklist no-match response as dead", () => {
|
|
166
|
+
__setRpcIdentityTestHooks({
|
|
167
|
+
platform: "win32",
|
|
168
|
+
execFileSync: (() =>
|
|
169
|
+
"INFO: No tasks are running which match the specified criteria.") as typeof execFileSync,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(isPidAlive(PID)).toBe("dead");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("returns inconclusive when the Windows tasklist probe cannot spawn", () => {
|
|
176
|
+
__setRpcIdentityTestHooks({
|
|
177
|
+
platform: "win32",
|
|
178
|
+
execFileSync: (() => {
|
|
179
|
+
throw new Error("tasklist unavailable");
|
|
180
|
+
}) as typeof execFileSync,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(isPidAlive(PID)).toBe("inconclusive");
|
|
184
|
+
expect(isPidIdentityPlausible(record(0))).toBe("inconclusive");
|
|
185
|
+
});
|
|
81
186
|
});
|
|
82
187
|
|
|
83
188
|
describe("isPidIdentityPlausible", () => {
|
|
@@ -99,7 +204,7 @@ describe("isPidIdentityPlausible", () => {
|
|
|
99
204
|
}) as typeof execFileSync,
|
|
100
205
|
});
|
|
101
206
|
|
|
102
|
-
expect(isPidIdentityPlausible(record(500_000))).toBe(
|
|
207
|
+
expect(isPidIdentityPlausible(record(500_000))).toBe("implausible");
|
|
103
208
|
expect(readPaths).toEqual([`/proc/${PID}/stat`, "/proc/uptime"]);
|
|
104
209
|
});
|
|
105
210
|
|
|
@@ -115,10 +220,10 @@ describe("isPidIdentityPlausible", () => {
|
|
|
115
220
|
|
|
116
221
|
// The mocked process start is 1,100,000ms. The 120s tolerance is part of
|
|
117
222
|
// the contract because port-file creation follows process startup.
|
|
118
|
-
expect(isPidIdentityPlausible(record(1_000_000))).toBe(
|
|
223
|
+
expect(isPidIdentityPlausible(record(1_000_000))).toBe("plausible");
|
|
119
224
|
});
|
|
120
225
|
|
|
121
|
-
test("
|
|
226
|
+
test("reports an unreadable Linux start-time probe as inconclusive", () => {
|
|
122
227
|
__setRpcIdentityTestHooks({
|
|
123
228
|
platform: "linux",
|
|
124
229
|
readFileSync: linuxFiles({
|
|
@@ -126,17 +231,17 @@ describe("isPidIdentityPlausible", () => {
|
|
|
126
231
|
}),
|
|
127
232
|
});
|
|
128
233
|
|
|
129
|
-
expect(isPidIdentityPlausible(record(500_000))).toBe(
|
|
234
|
+
expect(isPidIdentityPlausible(record(500_000))).toBe("inconclusive");
|
|
130
235
|
});
|
|
131
236
|
|
|
132
|
-
test("uses the legacy Linux command fallback and
|
|
237
|
+
test("uses the legacy Linux command fallback and distinguishes probe errors", () => {
|
|
133
238
|
__setRpcIdentityTestHooks({
|
|
134
239
|
platform: "linux",
|
|
135
240
|
readFileSync: linuxFiles({
|
|
136
241
|
[`/proc/${PID}/cmdline`]: "/usr/sbin/opendkim --config /etc/opendkim.conf",
|
|
137
242
|
}),
|
|
138
243
|
});
|
|
139
|
-
expect(isPidIdentityPlausible(record(0))).toBe(
|
|
244
|
+
expect(isPidIdentityPlausible(record(0))).toBe("implausible");
|
|
140
245
|
|
|
141
246
|
__setRpcIdentityTestHooks({
|
|
142
247
|
platform: "linux",
|
|
@@ -144,7 +249,7 @@ describe("isPidIdentityPlausible", () => {
|
|
|
144
249
|
[`/proc/${PID}/cmdline`]: "/usr/local/bin/opencode serve",
|
|
145
250
|
}),
|
|
146
251
|
});
|
|
147
|
-
expect(isPidIdentityPlausible(record(0))).toBe(
|
|
252
|
+
expect(isPidIdentityPlausible(record(0))).toBe("plausible");
|
|
148
253
|
|
|
149
254
|
__setRpcIdentityTestHooks({
|
|
150
255
|
platform: "linux",
|
|
@@ -152,7 +257,7 @@ describe("isPidIdentityPlausible", () => {
|
|
|
152
257
|
[`/proc/${PID}/cmdline`]: new Error("procfs unavailable"),
|
|
153
258
|
}),
|
|
154
259
|
});
|
|
155
|
-
expect(isPidIdentityPlausible(record(0))).toBe(
|
|
260
|
+
expect(isPidIdentityPlausible(record(0))).toBe("inconclusive");
|
|
156
261
|
});
|
|
157
262
|
|
|
158
263
|
test("uses ps start time and command probes on non-Linux platforms", () => {
|
|
@@ -162,7 +267,7 @@ describe("isPidIdentityPlausible", () => {
|
|
|
162
267
|
});
|
|
163
268
|
expect(
|
|
164
269
|
isPidIdentityPlausible(record(Date.parse("Mon Aug 7 00:00:00 1970") - 121_000)),
|
|
165
|
-
).toBe(
|
|
270
|
+
).toBe("implausible");
|
|
166
271
|
|
|
167
272
|
__setRpcIdentityTestHooks({
|
|
168
273
|
platform: "darwin",
|
|
@@ -170,24 +275,43 @@ describe("isPidIdentityPlausible", () => {
|
|
|
170
275
|
});
|
|
171
276
|
expect(
|
|
172
277
|
isPidIdentityPlausible(record(Date.parse("Mon Aug 7 00:00:00 1970") - 120_000)),
|
|
173
|
-
).toBe(
|
|
278
|
+
).toBe("plausible");
|
|
174
279
|
|
|
175
280
|
__setRpcIdentityTestHooks({
|
|
176
281
|
platform: "darwin",
|
|
177
282
|
execFileSync: psOutput("/usr/sbin/opendkim -f"),
|
|
178
283
|
});
|
|
179
|
-
expect(isPidIdentityPlausible(record(0))).toBe(
|
|
284
|
+
expect(isPidIdentityPlausible(record(0))).toBe("implausible");
|
|
180
285
|
|
|
181
286
|
__setRpcIdentityTestHooks({
|
|
182
287
|
platform: "darwin",
|
|
183
288
|
execFileSync: psOutput("/Applications/OpenCode.app/Contents/MacOS/opencode"),
|
|
184
289
|
});
|
|
185
|
-
expect(isPidIdentityPlausible(record(0))).toBe(
|
|
290
|
+
expect(isPidIdentityPlausible(record(0))).toBe("plausible");
|
|
186
291
|
|
|
187
292
|
__setRpcIdentityTestHooks({
|
|
188
293
|
platform: "darwin",
|
|
189
294
|
execFileSync: psOutput(new Error("ps unavailable")),
|
|
190
295
|
});
|
|
191
|
-
expect(isPidIdentityPlausible(record(0))).toBe(
|
|
296
|
+
expect(isPidIdentityPlausible(record(0))).toBe("inconclusive");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("uses tasklist for the Windows command fallback and skips unavailable start time", () => {
|
|
300
|
+
const calls: Array<{ file: string; args: readonly string[] }> = [];
|
|
301
|
+
__setRpcIdentityTestHooks({
|
|
302
|
+
platform: "win32",
|
|
303
|
+
execFileSync: ((file: string | URL, args: readonly string[] = []) => {
|
|
304
|
+
calls.push({ file: String(file), args });
|
|
305
|
+
if (String(file) === "ps") throw new Error("ps must not run on Windows");
|
|
306
|
+
return tasklistOutput([[PID, "OpenCode.exe"]]);
|
|
307
|
+
}) as typeof execFileSync,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(isPidIdentityPlausible(record(0))).toBe("plausible");
|
|
311
|
+
expect(calls).toEqual([{ file: "tasklist", args: ["/FO", "CSV", "/FI", `PID eq ${PID}`] }]);
|
|
312
|
+
|
|
313
|
+
calls.length = 0;
|
|
314
|
+
expect(isPidIdentityPlausible(record(NOW_MS))).toBe("inconclusive");
|
|
315
|
+
expect(calls).toEqual([]);
|
|
192
316
|
});
|
|
193
317
|
});
|