@cesarus777/opencode-cgp 0.2.2 → 0.2.3
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/README.md +3 -1
- package/dist/core.js +2 -1
- package/dist/index.js +80 -8
- package/dist/tui.js +2 -1
- package/dist/types/adapter.d.ts +1 -1
- package/dist/types/snapshotserver.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ Global configuration is `~/.config/opencode/opencode.jsonc` (or the OpenCode con
|
|
|
41
41
|
|
|
42
42
|
Restart/reload OpenCode and look for `context-guard: observe; diagnostics ...`. Observe mode records policy decisions and never requests compaction; OpenCode's own automatic compaction remains active. Inspect diagnostics first, then set `mode` to `enforce` to admit native compaction at runtime step boundaries. The complete [example](examples/opencode.jsonc) includes model, role, and agent policies. **Last matching rule wins**; selectors within a rule are ANDed.
|
|
43
43
|
|
|
44
|
-
Pin the exact published version in the existing entry's `package` field or use the manager. Version `0.2.
|
|
44
|
+
Pin the exact published version in the existing entry's `package` field or use the manager. Version `0.2.3` restores context usage for existing sessions when the plugin is enabled and keeps stale occupancy visible.
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
47
|
opencode plugin add @cesarus777/opencode-cgp@0.2.1
|
|
@@ -100,6 +100,8 @@ Open `/context-guard` or **Open Context Guard** in the command palette for curre
|
|
|
100
100
|
|
|
101
101
|
The TUI reads bounded snapshots over public OpenCode RPC, including when telemetry is disabled. Remote clients also need a client-side package registration in `cli.json`; see [the example](examples/cli.jsonc) and [controls, installation and bounds](docs/configuration.md#native-context-guard-view). It targets the inspected 2.0.29 API. The actual local host passes wide root, child and narrow grandchild panel checks. Use **Context Guard: open panel** above a child composer, where the host hides its usual prompt and palette shortcut. tmux, Neovim and cross-machine remote validation remain open.
|
|
102
102
|
|
|
103
|
+
Enabling Context Guard after work has already started also shows the last reported prompt usage for idle sessions and subagents. Opening the sidebar or panel restores it from the public session context, preserving its original age and **STALE** label. Stale usage still shows tokens and occupancy percentage. Restoration respects model changes, reverts and completed compactions; unavailable provider usage stays unknown. These reads never request compaction, and transcript content is never retained or sent through the plugin RPC.
|
|
104
|
+
|
|
103
105
|
Per-session live TPS, average model throughput, average session throughput and observed TTFT appear in the selected session and child rows. Live TPS and TTFT use `~` to mark estimates; completed rates use provider totals. Configurable local history defaults to seven days with a 64 MiB total cap, independently of diagnostic telemetry. See [metric definitions and history exports](docs/configuration.md#live-performance-and-retained-history).
|
|
104
106
|
|
|
105
107
|
Press **c** in the Context Guard panel for retained-history terminal charts. View one metric at a time with **Left / Right**, change the time window with **[ / ]**, and select an agent instance with **Up / Down**. Press **c** to return or **q** to close the panel; controls stay visible below the scrollable content. Generate a self-contained interactive browser report with `bun run charts /absolute/history-directory --output report.html`. It includes filters, UTC timestamp tooltips, request statistics and SVG export. Installed packages expose the same command as `context-guard charts ...` (Bun required). See [chart controls and limits](docs/configuration.md#performance-charts).
|
package/dist/core.js
CHANGED
|
@@ -748,13 +748,14 @@ function metric(snapshot, now, staleMs) {
|
|
|
748
748
|
const freshness = !m ? "missing" : m.model !== snapshot?.model || m.at > now ? "invalid" : now - m.at > staleMs ? "stale" : "fresh";
|
|
749
749
|
const t = snapshot?.policy.triggerTokens;
|
|
750
750
|
const valid = freshness === "fresh" && m !== undefined;
|
|
751
|
+
const displayable = valid || freshness === "stale" && m !== undefined;
|
|
751
752
|
const pressureRatio = valid && t !== undefined && t > 0 ? m.tokens / t : undefined;
|
|
752
753
|
const crossed = valid && t !== undefined && t > 0 && (snapshot?.policy.comparison === ">" ? m.tokens > t : m.tokens >= t);
|
|
753
754
|
return {
|
|
754
755
|
freshness,
|
|
755
756
|
pressure: pressureRatio === undefined ? "unknown" : crossed ? "crossed" : pressureRatio >= 0.9 ? "near" : "normal",
|
|
756
757
|
...pressureRatio === undefined ? {} : { pressureRatio },
|
|
757
|
-
...
|
|
758
|
+
...displayable && snapshot?.capacityTokens && snapshot.capacityTokens > 0 ? { utilizationPercent: m.tokens / snapshot.capacityTokens * 100 } : {},
|
|
758
759
|
...valid && t !== undefined ? { triggerDeltaTokens: t - m.tokens } : {}
|
|
759
760
|
};
|
|
760
761
|
}
|
package/dist/index.js
CHANGED
|
@@ -1330,7 +1330,7 @@ function freezeSnapshot(row2) {
|
|
|
1330
1330
|
}
|
|
1331
1331
|
|
|
1332
1332
|
// src/snapshotserver.ts
|
|
1333
|
-
async function serveSnapshots(rpc, store, performance = false) {
|
|
1333
|
+
async function serveSnapshots(rpc, store, performance = false, prepareRead) {
|
|
1334
1334
|
if (!rpc)
|
|
1335
1335
|
return () => {
|
|
1336
1336
|
};
|
|
@@ -1341,9 +1341,10 @@ async function serveSnapshots(rpc, store, performance = false) {
|
|
|
1341
1341
|
const batchValid = performance ? isPerformanceBatch : isBatch;
|
|
1342
1342
|
const resultValid = performance ? isPerformanceResult : isResult;
|
|
1343
1343
|
const registration = await rpc.register(performance ? performanceRpc : monitorRpc, {
|
|
1344
|
-
read: async (input) => {
|
|
1344
|
+
read: async (input, context) => {
|
|
1345
1345
|
if (!isRead(input))
|
|
1346
1346
|
throw new Error("Invalid snapshot request");
|
|
1347
|
+
await prepareRead?.(input.sessionIds, context.signal);
|
|
1347
1348
|
const raw = store.read(input);
|
|
1348
1349
|
const result = performance ? raw : stripPerformance(raw);
|
|
1349
1350
|
if (!resultValid(result))
|
|
@@ -2053,7 +2054,10 @@ async function setup(ctx, injectedSink) {
|
|
|
2053
2054
|
diagnostics.emit(event);
|
|
2054
2055
|
}
|
|
2055
2056
|
};
|
|
2056
|
-
|
|
2057
|
+
let prepareRead = async (_ids, _signal) => {
|
|
2058
|
+
};
|
|
2059
|
+
const onRead = (ids, signal) => prepareRead(ids, signal);
|
|
2060
|
+
const stopV1 = await serveSnapshots(ctx.rpc, store, false, onRead).catch(() => {
|
|
2057
2061
|
diagnostics.emit({
|
|
2058
2062
|
time: Date.now(),
|
|
2059
2063
|
type: "snapshot-service-unavailable"
|
|
@@ -2061,7 +2065,7 @@ async function setup(ctx, injectedSink) {
|
|
|
2061
2065
|
return () => {
|
|
2062
2066
|
};
|
|
2063
2067
|
});
|
|
2064
|
-
const stopV2 = await serveSnapshots(ctx.rpc, store, true).catch(() => {
|
|
2068
|
+
const stopV2 = await serveSnapshots(ctx.rpc, store, true, onRead).catch(() => {
|
|
2065
2069
|
diagnostics.emit({
|
|
2066
2070
|
time: Date.now(),
|
|
2067
2071
|
type: "performance-service-unavailable"
|
|
@@ -2179,8 +2183,73 @@ async function setup(ctx, injectedSink) {
|
|
|
2179
2183
|
}, sink);
|
|
2180
2184
|
monitor = new ContextMonitor(config, scheduler, sink);
|
|
2181
2185
|
const activeMonitor = monitor;
|
|
2186
|
+
const usageReads = new Map;
|
|
2187
|
+
prepareRead = (ids, signal) => serialized(async () => {
|
|
2188
|
+
if (stopped || signal.aborted || !ctx.session.context || !ids.length)
|
|
2189
|
+
return;
|
|
2190
|
+
const readSignal = AbortSignal.any([
|
|
2191
|
+
signal,
|
|
2192
|
+
controller.signal,
|
|
2193
|
+
AbortSignal.timeout(5000)
|
|
2194
|
+
]);
|
|
2195
|
+
for (const id of ids) {
|
|
2196
|
+
if (readSignal.aborted)
|
|
2197
|
+
break;
|
|
2198
|
+
const existing = activeMonitor.sessions.get(id);
|
|
2199
|
+
if (existing?.usage.current || existing?.active || (existing?.usage.modelRequests ?? 0) > 0 || recoveries.has(id))
|
|
2200
|
+
continue;
|
|
2201
|
+
if (existing && (existing.compaction.phase !== "idle" || existing.compaction.lastAttempt !== undefined || existing.compaction.lastCompleted !== undefined || existing.compaction.failures > 0))
|
|
2202
|
+
continue;
|
|
2203
|
+
if (Date.now() - (usageReads.get(id) ?? 0) < 30000)
|
|
2204
|
+
continue;
|
|
2205
|
+
if (usageReads.size >= config.safety.maxSessions) {
|
|
2206
|
+
const oldest = usageReads.keys().next().value;
|
|
2207
|
+
if (oldest !== undefined)
|
|
2208
|
+
usageReads.delete(oldest);
|
|
2209
|
+
}
|
|
2210
|
+
usageReads.set(id, Date.now());
|
|
2211
|
+
try {
|
|
2212
|
+
const info = await ctx.session.get({ sessionID: id }, { signal: readSignal });
|
|
2213
|
+
if (info.location.directory !== ctx.location.directory)
|
|
2214
|
+
continue;
|
|
2215
|
+
const messages = await ctx.session.context({ sessionID: id }, { signal: readSignal });
|
|
2216
|
+
const boundary = info.revert?.messageID;
|
|
2217
|
+
const end = boundary ? messages.findIndex((m) => m.id === boundary) : messages.length;
|
|
2218
|
+
if (end < 0)
|
|
2219
|
+
continue;
|
|
2220
|
+
const compacted = messages.findLastIndex((m, index) => index < end && m.type === "compaction" && m.status === "completed");
|
|
2221
|
+
const message = messages.findLast((m, index) => index > compacted && index < end && m.type === "assistant" && m.tokens !== undefined);
|
|
2222
|
+
if (message?.type !== "assistant" || !message.tokens)
|
|
2223
|
+
continue;
|
|
2224
|
+
const tokens = promptTokens(message.tokens);
|
|
2225
|
+
const model = `${message.model.providerID}/${message.model.id}`;
|
|
2226
|
+
const at = message.time.completed ?? message.time.streamed;
|
|
2227
|
+
if (tokens === undefined || at === undefined || !Number.isFinite(at) || at < 0 || at > Date.now())
|
|
2228
|
+
continue;
|
|
2229
|
+
if (info.model && model !== `${info.model.providerID}/${info.model.id}`)
|
|
2230
|
+
continue;
|
|
2231
|
+
await refreshModels(!models.has(model), readSignal);
|
|
2232
|
+
const s = await discover(id, readSignal);
|
|
2233
|
+
if (!s || s.model && s.model !== model || s.usage.current || readSignal.aborted)
|
|
2234
|
+
continue;
|
|
2235
|
+
activeMonitor.touch({ sessionId: id, model }, models.get(model));
|
|
2236
|
+
s.usage.current = {
|
|
2237
|
+
tokens,
|
|
2238
|
+
at,
|
|
2239
|
+
model,
|
|
2240
|
+
provenance: "provider-normalized",
|
|
2241
|
+
confidence: "reported",
|
|
2242
|
+
scope: "completed-request",
|
|
2243
|
+
requestId: message.id
|
|
2244
|
+
};
|
|
2245
|
+
s.usage.peak = Math.max(s.usage.peak, tokens);
|
|
2246
|
+
store.mark(id);
|
|
2247
|
+
} catch {
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
});
|
|
2182
2251
|
let models = new Map, modelAt = 0, modelRetryAt = 0, modelRetryMs = 1000;
|
|
2183
|
-
async function refreshModels(retryMissing = false) {
|
|
2252
|
+
async function refreshModels(retryMissing = false, signal = controller.signal) {
|
|
2184
2253
|
const now = Date.now();
|
|
2185
2254
|
if (now - modelAt < 60000 && (!retryMissing || now < modelRetryAt))
|
|
2186
2255
|
return;
|
|
@@ -2188,7 +2257,10 @@ async function setup(ctx, injectedSink) {
|
|
|
2188
2257
|
modelRetryAt = now + modelRetryMs;
|
|
2189
2258
|
modelRetryMs = Math.min(60000, modelRetryMs * 2);
|
|
2190
2259
|
try {
|
|
2191
|
-
models = new Map((await ctx.model.list(undefined, { signal
|
|
2260
|
+
models = new Map((await ctx.model.list(undefined, { signal })).data.map((m) => [
|
|
2261
|
+
`${m.providerID}/${m.id}`,
|
|
2262
|
+
{ ...m.limit }
|
|
2263
|
+
]));
|
|
2192
2264
|
let missing = false;
|
|
2193
2265
|
for (const s of activeMonitor.sessions.values()) {
|
|
2194
2266
|
const limits = s.model ? models.get(s.model) : undefined;
|
|
@@ -2212,8 +2284,8 @@ async function setup(ctx, injectedSink) {
|
|
|
2212
2284
|
sink.emit({ time: Date.now(), type: "model-metadata-unavailable" });
|
|
2213
2285
|
}
|
|
2214
2286
|
}
|
|
2215
|
-
async function discover(sessionId) {
|
|
2216
|
-
const info = await ctx.session.get({ sessionID: sessionId }, { signal
|
|
2287
|
+
async function discover(sessionId, signal = controller.signal) {
|
|
2288
|
+
const info = await ctx.session.get({ sessionID: sessionId }, { signal });
|
|
2217
2289
|
if (info.location.directory !== ctx.location.directory)
|
|
2218
2290
|
return;
|
|
2219
2291
|
const identity = {
|
package/dist/tui.js
CHANGED
|
@@ -330,13 +330,14 @@ function metric(snapshot, now, staleMs) {
|
|
|
330
330
|
const freshness = !m ? "missing" : m.model !== snapshot?.model || m.at > now ? "invalid" : now - m.at > staleMs ? "stale" : "fresh";
|
|
331
331
|
const t = snapshot?.policy.triggerTokens;
|
|
332
332
|
const valid2 = freshness === "fresh" && m !== undefined;
|
|
333
|
+
const displayable = valid2 || freshness === "stale" && m !== undefined;
|
|
333
334
|
const pressureRatio = valid2 && t !== undefined && t > 0 ? m.tokens / t : undefined;
|
|
334
335
|
const crossed = valid2 && t !== undefined && t > 0 && (snapshot?.policy.comparison === ">" ? m.tokens > t : m.tokens >= t);
|
|
335
336
|
return {
|
|
336
337
|
freshness,
|
|
337
338
|
pressure: pressureRatio === undefined ? "unknown" : crossed ? "crossed" : pressureRatio >= 0.9 ? "near" : "normal",
|
|
338
339
|
...pressureRatio === undefined ? {} : { pressureRatio },
|
|
339
|
-
...
|
|
340
|
+
...displayable && snapshot?.capacityTokens && snapshot.capacityTokens > 0 ? { utilizationPercent: m.tokens / snapshot.capacityTokens * 100 } : {},
|
|
340
341
|
...valid2 && t !== undefined ? { triggerDeltaTokens: t - m.tokens } : {}
|
|
341
342
|
};
|
|
342
343
|
}
|
package/dist/types/adapter.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface Host {
|
|
|
12
12
|
location: {
|
|
13
13
|
directory: string;
|
|
14
14
|
};
|
|
15
|
-
session: Pick<Plugin.Context["session"], "get" | "compact" | "hook"
|
|
15
|
+
session: Pick<Plugin.Context["session"], "get" | "compact" | "hook"> & Partial<Pick<Plugin.Context["session"], "context">>;
|
|
16
16
|
model: Pick<Plugin.Context["model"], "list">;
|
|
17
17
|
event: Plugin.Context["event"];
|
|
18
18
|
storage: Pick<Plugin.Context["storage"], "get" | "set">;
|
|
@@ -4,4 +4,4 @@ export interface SnapshotRpcPort {
|
|
|
4
4
|
}
|
|
5
5
|
import { monitorRpc, performanceRpc } from "./rpc.js";
|
|
6
6
|
import type { SnapshotStore } from "./snapshot.js";
|
|
7
|
-
export declare function serveSnapshots(rpc: SnapshotRpcPort | undefined, store: SnapshotStore, performance?: boolean): Promise<() => void>;
|
|
7
|
+
export declare function serveSnapshots(rpc: SnapshotRpcPort | undefined, store: SnapshotStore, performance?: boolean, prepareRead?: (ids: readonly string[], signal: AbortSignal) => Promise<void>): Promise<() => void>;
|