@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 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.2` adds live context-capacity charts, single-metric performance chart navigation, panel padding, and persistent close controls.
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
- ...valid && snapshot?.capacityTokens && snapshot.capacityTokens > 0 ? { utilizationPercent: m.tokens / snapshot.capacityTokens * 100 } : {},
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
- const stopV1 = await serveSnapshots(ctx.rpc, store).catch(() => {
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: controller.signal })).data.map((m) => [`${m.providerID}/${m.id}`, { ...m.limit }]));
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: controller.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
- ...valid2 && snapshot?.capacityTokens && snapshot.capacityTokens > 0 ? { utilizationPercent: m.tokens / snapshot.capacityTokens * 100 } : {},
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
  }
@@ -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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cesarus777/opencode-cgp",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {