@oar-sh/oar 0.9.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/.github/extensions/web-relay/README.md +190 -0
- package/.github/extensions/web-relay/extension.mjs +972 -0
- package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
- package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
- package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
- package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
- package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
- package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
- package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
- package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
- package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
- package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
- package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
- package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
- package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
- package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
- package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
- package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
- package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
- package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
- package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
- package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
- package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/bin/bat/oar.bat +46 -0
- package/bin/oar.js +665 -0
- package/package.json +68 -0
- package/server/README.md +1379 -0
- package/server/claude-worker/claude-attachments.mjs +134 -0
- package/server/claude-worker/claude-preview-tool.mjs +57 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
- package/server/claude-worker/claude-session-process.mjs +2391 -0
- package/server/claude-worker/claude-session-test-harness.mjs +236 -0
- package/server/claude-worker/claude-session-worker.mjs +186 -0
- package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
- package/server/claude-worker/claude-turn-publisher.mjs +291 -0
- package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
- package/server/claude-worker/workflow-progress-digest.mjs +269 -0
- package/server/copilot-sdk-runtime.mjs +126 -0
- package/server/copilot-worker/copilot-attachments.mjs +97 -0
- package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
- package/server/copilot-worker/copilot-plan-board.mjs +110 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
- package/server/copilot-worker/fixtures/abort-turn.json +588 -0
- package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
- package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
- package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
- package/server/copilot-worker/fixtures/happy-turn.json +566 -0
- package/server/copilot-worker/fixtures/quota-turn.json +647 -0
- package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
- package/server/copilot-worker/fixtures/resume-turn.json +700 -0
- package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
- package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
- package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
- package/server/cursor-worker/cursor-attachments.mjs +88 -0
- package/server/cursor-worker/cursor-context-usage.mjs +85 -0
- package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
- package/server/cursor-worker/cursor-session-worker.mjs +145 -0
- package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
- package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
- package/server/db-schema.mjs +932 -0
- package/server/features.mjs +106 -0
- package/server/grok-worker/acp-client.mjs +434 -0
- package/server/grok-worker/acp-host-services.mjs +277 -0
- package/server/grok-worker/grok-context-usage.mjs +77 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
- package/server/grok-worker/grok-session-worker.mjs +145 -0
- package/server/grok-worker/grok-turn-runner.mjs +523 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
- package/server/migrations/0001-add-structured-answer.mjs +80 -0
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
- package/server/migrations/0003-push-subscriptions.mjs +87 -0
- package/server/public/app/action-confirmations.js +267 -0
- package/server/public/app/activity-replay-state.mjs +149 -0
- package/server/public/app/api-client.js +1082 -0
- package/server/public/app/ask-user-view.js +545 -0
- package/server/public/app/attachments-view.js +2112 -0
- package/server/public/app/auto-compact-window-options.mjs +59 -0
- package/server/public/app/background-tasks-view.mjs +494 -0
- package/server/public/app/bootstrap.js +4347 -0
- package/server/public/app/claude-auth-ui.js +394 -0
- package/server/public/app/claude-thinking-options.mjs +38 -0
- package/server/public/app/cli-install-ui.js +455 -0
- package/server/public/app/code-copy.mjs +85 -0
- package/server/public/app/composer-attachment-cache.mjs +151 -0
- package/server/public/app/composer-control-state.mjs +107 -0
- package/server/public/app/composer-paste.mjs +132 -0
- package/server/public/app/context-tier-options.mjs +68 -0
- package/server/public/app/context-usage-view.mjs +338 -0
- package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
- package/server/public/app/conversation-preferences.mjs +91 -0
- package/server/public/app/conversation-provider-indicator.mjs +88 -0
- package/server/public/app/conversation-view.js +2898 -0
- package/server/public/app/cwd-menu-placement.mjs +85 -0
- package/server/public/app/cwd-picker.js +605 -0
- package/server/public/app/emoji-view.js +101 -0
- package/server/public/app/external-link-policy.mjs +80 -0
- package/server/public/app/file-preview-navigation.mjs +51 -0
- package/server/public/app/font-scaling.js +262 -0
- package/server/public/app/git-changes-view.js +339 -0
- package/server/public/app/git-diff-model.mjs +109 -0
- package/server/public/app/grok-auth-ui.js +387 -0
- package/server/public/app/image-downscale.mjs +174 -0
- package/server/public/app/in-flight-snapshot.mjs +40 -0
- package/server/public/app/infinite-loader.js +164 -0
- package/server/public/app/journal-view.js +1141 -0
- package/server/public/app/known-cwd-options.mjs +41 -0
- package/server/public/app/launch-cwd-path.mjs +23 -0
- package/server/public/app/live-message-dedupe.mjs +57 -0
- package/server/public/app/message-search-view.js +242 -0
- package/server/public/app/model-selector-options.mjs +31 -0
- package/server/public/app/model-vision-support.mjs +56 -0
- package/server/public/app/new-conversation-model-choice.mjs +67 -0
- package/server/public/app/plan-usage-view.mjs +307 -0
- package/server/public/app/preview-cards.mjs +273 -0
- package/server/public/app/preview-command.mjs +103 -0
- package/server/public/app/push-settings.js +370 -0
- package/server/public/app/pwa-install.js +456 -0
- package/server/public/app/question-schema-view.mjs +71 -0
- package/server/public/app/reasoning-effort-labels.mjs +42 -0
- package/server/public/app/relay-board-view.js +117 -0
- package/server/public/app/relay-dot-state.mjs +69 -0
- package/server/public/app/relay-error-ctas.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
- package/server/public/app/relay-thoughts.mjs +98 -0
- package/server/public/app/relay-watchdog-policy.mjs +29 -0
- package/server/public/app/repo-browser-preferences.mjs +79 -0
- package/server/public/app/repo-browser-tree-state.mjs +97 -0
- package/server/public/app/router.js +657 -0
- package/server/public/app/selection-guard.mjs +93 -0
- package/server/public/app/settings-modal.js +1484 -0
- package/server/public/app/settings-tabs.js +129 -0
- package/server/public/app/slash-autocomplete.mjs +188 -0
- package/server/public/app/slash-commands.mjs +183 -0
- package/server/public/app/socket-handlers.js +703 -0
- package/server/public/app/status-store.mjs +242 -0
- package/server/public/app/status-view.mjs +315 -0
- package/server/public/app/store.js +1454 -0
- package/server/public/app/stream-state.mjs +73 -0
- package/server/public/app/streaming-dom-patch.mjs +23 -0
- package/server/public/app/sync-outbox.mjs +208 -0
- package/server/public/app/tap-actions.js +62 -0
- package/server/public/app/thread-order.mjs +54 -0
- package/server/public/app/tmux-inspector-view.js +422 -0
- package/server/public/app/transcript-separators.mjs +282 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon.svg +4 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +5743 -0
- package/server/public/manifest.webmanifest +32 -0
- package/server/public/sw.js +307 -0
- package/server/relay-exit-codes.mjs +4 -0
- package/server/relay-self-restart.mjs +170 -0
- package/server/relay-tools.md +27 -0
- package/server/relay.mjs +1026 -0
- package/server/repositories/image-conversation-repository.mjs +317 -0
- package/server/repositories/message-repository.mjs +434 -0
- package/server/repositories/question-repository.mjs +161 -0
- package/server/repositories/session-repository.mjs +320 -0
- package/server/repositories/share-visibility-statements.mjs +24 -0
- package/server/routes/ask-user-routes.mjs +292 -0
- package/server/routes/cache-routes.mjs +44 -0
- package/server/routes/git-routes.mjs +92 -0
- package/server/routes/messages-routes-test-harness.mjs +111 -0
- package/server/routes/messages-routes.mjs +7107 -0
- package/server/routes/preview-routes.mjs +108 -0
- package/server/routes/push-routes.mjs +59 -0
- package/server/routes/relay-board-routes.mjs +268 -0
- package/server/routes/sessions-routes.mjs +6150 -0
- package/server/server-runtime.mjs +7463 -0
- package/server/server.js +24 -0
- package/server/services/ask-user-routing-service.mjs +117 -0
- package/server/services/cache-rebuild-service.mjs +236 -0
- package/server/services/claude-auth-service.mjs +578 -0
- package/server/services/claude-context-usage.mjs +240 -0
- package/server/services/claude-session-root-service.mjs +228 -0
- package/server/services/cli-install-service.mjs +1152 -0
- package/server/services/cli-process-runner.mjs +181 -0
- package/server/services/cloudflared-tunnel-service.mjs +358 -0
- package/server/services/context-snapshot-service.mjs +499 -0
- package/server/services/context-usage-view.mjs +160 -0
- package/server/services/conversation-preferences-service.mjs +208 -0
- package/server/services/cursor-dashboard-usage.mjs +217 -0
- package/server/services/cursor-session-root-service.mjs +62 -0
- package/server/services/delete-archive-service.mjs +154 -0
- package/server/services/drives-path-helpers.mjs +66 -0
- package/server/services/fixtures/claude-auth-stub.sh +62 -0
- package/server/services/fixtures/cli-install-stub.sh +75 -0
- package/server/services/fixtures/grok-stub.sh +86 -0
- package/server/services/generated-image-cleanup-service.mjs +114 -0
- package/server/services/git-changes-service.mjs +195 -0
- package/server/services/github-billing-usage.mjs +133 -0
- package/server/services/grok-auth-service.mjs +593 -0
- package/server/services/grok-billing-usage.mjs +120 -0
- package/server/services/image-context-service.mjs +106 -0
- package/server/services/image-operation-service.mjs +135 -0
- package/server/services/keyed-mutex-service.mjs +82 -0
- package/server/services/mime-sniffer.mjs +118 -0
- package/server/services/oar-cli-helpers.mjs +109 -0
- package/server/services/oar-state-migration-service.mjs +243 -0
- package/server/services/plan-usage-claude.mjs +387 -0
- package/server/services/plan-usage-contract.mjs +271 -0
- package/server/services/plan-usage-copilot.mjs +421 -0
- package/server/services/plan-usage-cursor.mjs +566 -0
- package/server/services/plan-usage-grok.mjs +436 -0
- package/server/services/plan-usage-service.mjs +487 -0
- package/server/services/preview-health-probe.mjs +82 -0
- package/server/services/preview-proxy-server.mjs +498 -0
- package/server/services/preview-registry-service.mjs +457 -0
- package/server/services/preview-static-handler.mjs +237 -0
- package/server/services/provider-model-selection.mjs +91 -0
- package/server/services/provider-reasoning-effort.mjs +94 -0
- package/server/services/push-active-device-service.mjs +57 -0
- package/server/services/push-dispatch-service.mjs +459 -0
- package/server/services/rate-limit-service.mjs +63 -0
- package/server/services/relay-bridge-owner-service.mjs +84 -0
- package/server/services/relay-cli-launcher-service.mjs +209 -0
- package/server/services/relay-prompt-sanitizer.mjs +137 -0
- package/server/services/relay-queue-gate-service.mjs +38 -0
- package/server/services/relay-restart-orchestrator-service.mjs +935 -0
- package/server/services/relay-singleton-guard.mjs +124 -0
- package/server/services/safe-served-content.mjs +57 -0
- package/server/services/sdk-session-import-service.mjs +271 -0
- package/server/services/sdk-session-sync-service.mjs +207 -0
- package/server/services/session-history-refresh-service.mjs +497 -0
- package/server/services/session-transcript-service.mjs +419 -0
- package/server/services/session-worker-launch-service.mjs +786 -0
- package/server/services/session-worker-process-service.mjs +319 -0
- package/server/services/session-worker-registry-service.mjs +194 -0
- package/server/services/session-worker-stop-service.mjs +141 -0
- package/server/services/session-worker-supervisor-service.mjs +898 -0
- package/server/services/session-worker-websocket-service.mjs +483 -0
- package/server/services/ssh-tunnel-manager-service.mjs +423 -0
- package/server/services/status-event-service.mjs +158 -0
- package/server/services/tmux-inspector-access-policy.mjs +77 -0
- package/server/services/tmux-inspector-socket-service.mjs +191 -0
- package/server/services/tmux-inspector-stream-service.mjs +552 -0
- package/server/services/tunnel-worker-path-guard.mjs +107 -0
- package/server/services/upload-sweep.mjs +60 -0
- package/server/services/usage-snapshot-helpers.mjs +209 -0
- package/server/services/windows-autostart-service.mjs +115 -0
- package/server/services/workspace-root-defaults-service.mjs +34 -0
- package/server/services/workspace-root-path-policy.mjs +196 -0
- package/server/services/workspace-root-relaunch-service.mjs +132 -0
- package/server/services/workspace-symlink-guard.mjs +54 -0
- package/server/tools/capture-proxy.mjs +173 -0
- package/server/tty-console-bootstrap.mjs +75 -0
- package/server/uploads/README.md +36 -0
- package/server/uploads/image_input.md +540 -0
- package/server/windows-terminal-launcher.mjs +29 -0
- package/server/workspace-root.mjs +87 -0
- package/shared/ask-user-bridge.mjs +131 -0
- package/shared/auto-compact-window.mjs +116 -0
- package/shared/background-task-timeout.mjs +52 -0
- package/shared/claude-thinking.mjs +101 -0
- package/shared/context-window-fallbacks.mjs +148 -0
- package/shared/control-poller.mjs +115 -0
- package/shared/empty-turn-completion.mjs +20 -0
- package/shared/model-catalog-freshness.mjs +11 -0
- package/shared/model-descriptors.mjs +118 -0
- package/shared/model-id.mjs +147 -0
- package/shared/model-refresh.mjs +24 -0
- package/shared/openai-reasoning.mjs +13 -0
- package/shared/plan-lines.mjs +13 -0
- package/shared/preview-instructions.mjs +77 -0
- package/shared/preview-tool-core.mjs +191 -0
- package/shared/provider-routing.mjs +29 -0
- package/shared/question-schema.mjs +315 -0
- package/shared/question-timeout.mjs +18 -0
- package/shared/stream-emit-gating.mjs +28 -0
- package/shared/subagent-run-id.mjs +17 -0
- package/shared/thought-cap.mjs +13 -0
- package/shared/tool-activity.mjs +98 -0
- package/shared/turn-ceiling.mjs +53 -0
- package/shared/worker-bootstrap.mjs +44 -0
- package/shared/worker-crash-guard.mjs +60 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { resolveFallbackContextLimitTokens } from '../../shared/context-window-fallbacks.mjs';
|
|
4
|
+
|
|
5
|
+
// Re-exported for existing importers; the table itself lives in shared/ so the
|
|
6
|
+
// Cursor worker's fallback cannot drift from this one.
|
|
7
|
+
export { resolveFallbackContextLimitTokens };
|
|
8
|
+
|
|
9
|
+
const CONTEXT_CACHE_MAX = 128;
|
|
10
|
+
|
|
11
|
+
function normalizeText(value) {
|
|
12
|
+
const text = String(value || '').trim();
|
|
13
|
+
return text || '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toNullableInt(value) {
|
|
17
|
+
if (value === null || value === undefined || value === '') return null;
|
|
18
|
+
const n = Number(value);
|
|
19
|
+
if (!Number.isFinite(n)) return null;
|
|
20
|
+
return Math.round(n);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toNullablePercent(value) {
|
|
24
|
+
if (value === null || value === undefined || value === '') return null;
|
|
25
|
+
const n = Number(value);
|
|
26
|
+
if (!Number.isFinite(n)) return null;
|
|
27
|
+
return Math.round(n * 100) / 100;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getByPath(obj, parts) {
|
|
31
|
+
let current = obj;
|
|
32
|
+
for (const part of (parts || [])) {
|
|
33
|
+
if (!current || typeof current !== 'object' || !(part in current)) return undefined;
|
|
34
|
+
current = current[part];
|
|
35
|
+
}
|
|
36
|
+
return current;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findFirstNumericByKey(obj, candidateKeys) {
|
|
40
|
+
if (!obj || typeof obj !== 'object') return null;
|
|
41
|
+
const wanted = new Set((candidateKeys || []).map((key) => normalizeText(key)).filter(Boolean));
|
|
42
|
+
if (!wanted.size) return null;
|
|
43
|
+
|
|
44
|
+
const stack = [obj];
|
|
45
|
+
while (stack.length) {
|
|
46
|
+
const current = stack.pop();
|
|
47
|
+
if (!current || typeof current !== 'object') continue;
|
|
48
|
+
for (const [key, value] of Object.entries(current)) {
|
|
49
|
+
if (value && typeof value === 'object') stack.push(value);
|
|
50
|
+
if (!wanted.has(key)) continue;
|
|
51
|
+
const numeric = Number(value);
|
|
52
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveContextLimitTokens(modelId, data, modelUsage, getModelContextLimitTokens = null) {
|
|
59
|
+
const directCandidates = [
|
|
60
|
+
getByPath(data, ['maxContextTokens']),
|
|
61
|
+
getByPath(data, ['contextWindow']),
|
|
62
|
+
getByPath(data, ['maxTokens']),
|
|
63
|
+
getByPath(data, ['contextLimitTokens']),
|
|
64
|
+
getByPath(data, ['max_context_tokens']),
|
|
65
|
+
getByPath(data, ['tokenBudget']),
|
|
66
|
+
getByPath(modelUsage, ['maxContextTokens']),
|
|
67
|
+
getByPath(modelUsage, ['contextWindow']),
|
|
68
|
+
getByPath(modelUsage, ['maxTokens']),
|
|
69
|
+
];
|
|
70
|
+
for (const value of directCandidates) {
|
|
71
|
+
const numeric = Number(value);
|
|
72
|
+
if (Number.isFinite(numeric) && numeric > 0) return Math.round(numeric);
|
|
73
|
+
}
|
|
74
|
+
const model = normalizeText(modelId).toLowerCase();
|
|
75
|
+
const catalogLimit = typeof getModelContextLimitTokens === 'function'
|
|
76
|
+
? toNullableInt(getModelContextLimitTokens(model))
|
|
77
|
+
: null;
|
|
78
|
+
if (catalogLimit !== null && catalogLimit > 0) return catalogLimit;
|
|
79
|
+
return resolveFallbackContextLimitTokens(model);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveExplicitContextLimitTokens(data, modelUsage) {
|
|
83
|
+
const directCandidates = [
|
|
84
|
+
getByPath(data, ['maxContextTokens']),
|
|
85
|
+
getByPath(data, ['contextWindow']),
|
|
86
|
+
getByPath(data, ['maxTokens']),
|
|
87
|
+
getByPath(data, ['contextLimitTokens']),
|
|
88
|
+
getByPath(data, ['max_context_tokens']),
|
|
89
|
+
getByPath(data, ['tokenBudget']),
|
|
90
|
+
getByPath(modelUsage, ['maxContextTokens']),
|
|
91
|
+
getByPath(modelUsage, ['contextWindow']),
|
|
92
|
+
getByPath(modelUsage, ['maxTokens']),
|
|
93
|
+
];
|
|
94
|
+
for (const value of directCandidates) {
|
|
95
|
+
const numeric = Number(value);
|
|
96
|
+
if (Number.isFinite(numeric) && numeric > 0) return Math.round(numeric);
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function extractSessionIdFromEventsPath(pathModule, eventsPath) {
|
|
102
|
+
const parent = pathModule.basename(pathModule.dirname(String(eventsPath || '')));
|
|
103
|
+
return parent || null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function hasContextFields(data) {
|
|
107
|
+
return (
|
|
108
|
+
Number.isFinite(Number(data?.currentTokens)) ||
|
|
109
|
+
Number.isFinite(Number(data?.systemTokens)) ||
|
|
110
|
+
Number.isFinite(Number(data?.conversationTokens)) ||
|
|
111
|
+
Number.isFinite(Number(data?.toolDefinitionsTokens))
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hasUsageSignals(data) {
|
|
116
|
+
return hasContextFields(data)
|
|
117
|
+
|| Number.isFinite(Number(data?.inputTokens))
|
|
118
|
+
|| Number.isFinite(Number(data?.outputTokens))
|
|
119
|
+
|| Number.isFinite(Number(data?.reasoningTokens))
|
|
120
|
+
|| Number.isFinite(Number(data?.cacheReadTokens))
|
|
121
|
+
|| Number.isFinite(Number(data?.cacheWriteTokens));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveEventModelId(data) {
|
|
125
|
+
return normalizeText(data?.currentModel || data?.model || data?.newModel) || null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function hasSnapshotSignals(data) {
|
|
129
|
+
const currentModel = resolveEventModelId(data);
|
|
130
|
+
const modelUsage = currentModel && data?.modelMetrics?.[currentModel]?.usage && typeof data.modelMetrics[currentModel].usage === 'object'
|
|
131
|
+
? data.modelMetrics[currentModel].usage
|
|
132
|
+
: null;
|
|
133
|
+
return hasContextFields(data)
|
|
134
|
+
|| resolveExplicitContextLimitTokens(data, modelUsage) !== null
|
|
135
|
+
|| Number.isFinite(Number(data?.usedPercent))
|
|
136
|
+
|| Number.isFinite(Number(data?.freeTokens))
|
|
137
|
+
|| Number.isFinite(Number(data?.remainingTokens))
|
|
138
|
+
|| Number.isFinite(Number(data?.bufferTokens))
|
|
139
|
+
|| Number.isFinite(Number(data?.inputTokens))
|
|
140
|
+
|| Number.isFinite(Number(data?.reasoningTokens))
|
|
141
|
+
|| Number.isFinite(Number(data?.cacheReadTokens))
|
|
142
|
+
|| Number.isFinite(Number(data?.cacheWriteTokens))
|
|
143
|
+
|| Number.isFinite(Number(modelUsage?.inputTokens))
|
|
144
|
+
|| Number.isFinite(Number(modelUsage?.reasoningTokens))
|
|
145
|
+
|| Number.isFinite(Number(modelUsage?.cacheReadTokens))
|
|
146
|
+
|| Number.isFinite(Number(modelUsage?.cacheWriteTokens));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildSnapshotFromEvent({ event, data, eventsPath, sessionId, pathModule, getModelContextLimitTokens }) {
|
|
150
|
+
const currentModel = resolveEventModelId(data);
|
|
151
|
+
const copilotSessionId = extractSessionIdFromEventsPath(pathModule, eventsPath) || sessionId;
|
|
152
|
+
const modelUsage = currentModel && data?.modelMetrics?.[currentModel]?.usage && typeof data.modelMetrics[currentModel].usage === 'object'
|
|
153
|
+
? data.modelMetrics[currentModel].usage
|
|
154
|
+
: null;
|
|
155
|
+
const systemTokens = toNullableInt(data?.systemTokens);
|
|
156
|
+
const conversationTokens = toNullableInt(data?.conversationTokens);
|
|
157
|
+
const toolsTokens = toNullableInt(data?.toolDefinitionsTokens);
|
|
158
|
+
const promptTokens = toNullableInt(modelUsage?.inputTokens) ?? toNullableInt(data?.inputTokens);
|
|
159
|
+
const completionTokens = toNullableInt(modelUsage?.outputTokens) ?? toNullableInt(data?.outputTokens);
|
|
160
|
+
const reasoningTokens = toNullableInt(modelUsage?.reasoningTokens) ?? toNullableInt(data?.reasoningTokens);
|
|
161
|
+
const usedTotalTokens = toNullableInt(data?.currentTokens)
|
|
162
|
+
?? toNullableInt(findFirstNumericByKey(data, ['usedTokens', 'totalTokens', 'usedTotalTokens']))
|
|
163
|
+
?? ((systemTokens !== null || conversationTokens !== null || toolsTokens !== null)
|
|
164
|
+
? (Number(systemTokens || 0) + Number(conversationTokens || 0) + Number(toolsTokens || 0))
|
|
165
|
+
: null)
|
|
166
|
+
?? ((promptTokens !== null || completionTokens !== null || reasoningTokens !== null)
|
|
167
|
+
? (Number(promptTokens || 0) + Number(completionTokens || 0) + Number(reasoningTokens || 0))
|
|
168
|
+
: null);
|
|
169
|
+
const contextLimitTokens = resolveContextLimitTokens(currentModel, data, modelUsage, getModelContextLimitTokens);
|
|
170
|
+
const usedPercent = toNullablePercent(data?.usedPercent)
|
|
171
|
+
?? toNullablePercent(findFirstNumericByKey(data, ['usedPercent', 'contextUsagePercent', 'tokenUsagePercent']))
|
|
172
|
+
?? ((usedTotalTokens !== null && contextLimitTokens !== null && contextLimitTokens > 0)
|
|
173
|
+
? Math.round((usedTotalTokens / contextLimitTokens) * 10000) / 100
|
|
174
|
+
: null);
|
|
175
|
+
const reportedFreeTokens = toNullableInt(data?.freeTokens)
|
|
176
|
+
?? toNullableInt(data?.remainingTokens)
|
|
177
|
+
?? toNullableInt(findFirstNumericByKey(data, ['freeTokens', 'remainingTokens', 'availableTokens']));
|
|
178
|
+
const freeTokens = reportedFreeTokens
|
|
179
|
+
?? ((usedTotalTokens !== null && contextLimitTokens !== null)
|
|
180
|
+
? Math.max(0, contextLimitTokens - usedTotalTokens)
|
|
181
|
+
: null);
|
|
182
|
+
// A remainder the CLI reported is already net of its own buffer; only the
|
|
183
|
+
// fallback above folds the buffer into free space.
|
|
184
|
+
const freeTokensIncludesBuffer = reportedFreeTokens === null && freeTokens !== null;
|
|
185
|
+
const bufferTokens = toNullableInt(data?.bufferTokens)
|
|
186
|
+
?? toNullableInt(findFirstNumericByKey(data, ['bufferTokens', 'safeBufferTokens']))
|
|
187
|
+
?? null;
|
|
188
|
+
const systemToolsTokens = ((systemTokens !== null || toolsTokens !== null)
|
|
189
|
+
? Number(systemTokens || 0) + Number(toolsTokens || 0)
|
|
190
|
+
: null);
|
|
191
|
+
const cacheReadTokens = toNullableInt(modelUsage?.cacheReadTokens)
|
|
192
|
+
?? toNullableInt(data?.cacheReadTokens)
|
|
193
|
+
?? toNullableInt(data?.compactionTokensUsed?.cacheReadTokens)
|
|
194
|
+
?? null;
|
|
195
|
+
const cacheWriteTokens = toNullableInt(modelUsage?.cacheWriteTokens)
|
|
196
|
+
?? toNullableInt(data?.cacheWriteTokens)
|
|
197
|
+
?? toNullableInt(data?.compactionTokensUsed?.cacheWriteTokens)
|
|
198
|
+
?? null;
|
|
199
|
+
const partialMetricsWarning = !hasContextFields(data)
|
|
200
|
+
? 'Legacy context-window metrics are unavailable in current session events; showing partial token data only.'
|
|
201
|
+
: null;
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
snapshot: {
|
|
205
|
+
runtime_session_id: sessionId,
|
|
206
|
+
copilot_session_id: copilotSessionId,
|
|
207
|
+
model: currentModel,
|
|
208
|
+
used_total_tokens: usedTotalTokens,
|
|
209
|
+
max_context_tokens: contextLimitTokens,
|
|
210
|
+
used_percent: usedPercent,
|
|
211
|
+
free_tokens: freeTokens,
|
|
212
|
+
free_tokens_includes_buffer: freeTokensIncludesBuffer,
|
|
213
|
+
buffer_tokens: bufferTokens,
|
|
214
|
+
system_tokens: systemTokens,
|
|
215
|
+
messages_tokens: conversationTokens,
|
|
216
|
+
tools_tokens: toolsTokens,
|
|
217
|
+
system_tools_tokens: toNullableInt(systemToolsTokens),
|
|
218
|
+
used_prompt_tokens: promptTokens,
|
|
219
|
+
used_completion_tokens: completionTokens,
|
|
220
|
+
reasoning_tokens: reasoningTokens,
|
|
221
|
+
cache_read_tokens: cacheReadTokens,
|
|
222
|
+
cache_write_tokens: cacheWriteTokens,
|
|
223
|
+
captured_at: normalizeText(event?.timestamp) || null,
|
|
224
|
+
estimate_kind: null,
|
|
225
|
+
},
|
|
226
|
+
error: partialMetricsWarning,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function createEstimateState(seed = null) {
|
|
231
|
+
return {
|
|
232
|
+
totalCompletionTokens: Number(seed?.totalCompletionTokens || 0),
|
|
233
|
+
latestTimestamp: normalizeText(seed?.latestTimestamp) || null,
|
|
234
|
+
latestModel: normalizeText(seed?.latestModel) || null,
|
|
235
|
+
explicitContextLimitTokens: toNullableInt(seed?.explicitContextLimitTokens),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function updateEstimateState(estimate, event, data) {
|
|
240
|
+
if (!estimate || !data || typeof data !== 'object') return estimate;
|
|
241
|
+
const next = estimate;
|
|
242
|
+
const modelId = resolveEventModelId(data);
|
|
243
|
+
const timestamp = normalizeText(event?.timestamp) || null;
|
|
244
|
+
if (modelId) next.latestModel = modelId;
|
|
245
|
+
if (timestamp) next.latestTimestamp = timestamp;
|
|
246
|
+
const currentModel = modelId;
|
|
247
|
+
const modelUsage = currentModel && data?.modelMetrics?.[currentModel]?.usage && typeof data.modelMetrics[currentModel].usage === 'object'
|
|
248
|
+
? data.modelMetrics[currentModel].usage
|
|
249
|
+
: null;
|
|
250
|
+
const explicitContextLimitTokens = resolveExplicitContextLimitTokens(data, modelUsage);
|
|
251
|
+
if (explicitContextLimitTokens !== null) next.explicitContextLimitTokens = explicitContextLimitTokens;
|
|
252
|
+
if (String(event?.type || '').trim() !== 'assistant.message') return next;
|
|
253
|
+
const completionTokens = toNullableInt(modelUsage?.outputTokens) ?? toNullableInt(data?.outputTokens);
|
|
254
|
+
if (completionTokens !== null) next.totalCompletionTokens += completionTokens;
|
|
255
|
+
return next;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function buildEstimatedSnapshot({ estimate, eventsPath, sessionId, pathModule, getModelContextLimitTokens }) {
|
|
259
|
+
const totalCompletionTokens = toNullableInt(estimate?.totalCompletionTokens);
|
|
260
|
+
if (totalCompletionTokens === null || totalCompletionTokens <= 0) return null;
|
|
261
|
+
const copilotSessionId = extractSessionIdFromEventsPath(pathModule, eventsPath) || sessionId;
|
|
262
|
+
const model = normalizeText(estimate?.latestModel) || null;
|
|
263
|
+
const contextLimitTokens = toNullableInt(estimate?.explicitContextLimitTokens)
|
|
264
|
+
?? resolveContextLimitTokens(model, null, null, getModelContextLimitTokens);
|
|
265
|
+
// Cumulative assistant output grows without bound while compaction keeps the
|
|
266
|
+
// real window in check, so this ratio can exceed 1. Clamp it: an occupancy
|
|
267
|
+
// gauge reading "340%" is worse than one pinned at 100%.
|
|
268
|
+
const usedPercent = (contextLimitTokens !== null && contextLimitTokens > 0)
|
|
269
|
+
? Math.min(100, Math.round((totalCompletionTokens / contextLimitTokens) * 10000) / 100)
|
|
270
|
+
: null;
|
|
271
|
+
const freeTokens = (contextLimitTokens !== null)
|
|
272
|
+
? Math.max(0, contextLimitTokens - totalCompletionTokens)
|
|
273
|
+
: null;
|
|
274
|
+
return {
|
|
275
|
+
snapshot: {
|
|
276
|
+
runtime_session_id: sessionId,
|
|
277
|
+
copilot_session_id: copilotSessionId,
|
|
278
|
+
model,
|
|
279
|
+
used_total_tokens: totalCompletionTokens,
|
|
280
|
+
max_context_tokens: contextLimitTokens,
|
|
281
|
+
used_percent: usedPercent,
|
|
282
|
+
free_tokens: freeTokens,
|
|
283
|
+
buffer_tokens: null,
|
|
284
|
+
system_tokens: null,
|
|
285
|
+
messages_tokens: totalCompletionTokens,
|
|
286
|
+
tools_tokens: null,
|
|
287
|
+
system_tools_tokens: null,
|
|
288
|
+
used_prompt_tokens: null,
|
|
289
|
+
used_completion_tokens: totalCompletionTokens,
|
|
290
|
+
reasoning_tokens: null,
|
|
291
|
+
cache_read_tokens: null,
|
|
292
|
+
cache_write_tokens: null,
|
|
293
|
+
captured_at: normalizeText(estimate?.latestTimestamp) || null,
|
|
294
|
+
estimate_kind: 'assistant-output-lower-bound',
|
|
295
|
+
},
|
|
296
|
+
error: 'Legacy context-window metrics are unavailable in current session events; showing a lower-bound estimate from cumulative assistant completion tokens only. Prompt, system/tool, cache, and buffer tokens are unavailable.',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function hydrateSnapshotFromEstimate(snapshot, estimate, getModelContextLimitTokens = null) {
|
|
301
|
+
if (!snapshot || typeof snapshot !== 'object') return snapshot;
|
|
302
|
+
const model = normalizeText(snapshot.model) || normalizeText(estimate?.latestModel) || null;
|
|
303
|
+
const contextLimitTokens = toNullableInt(snapshot.max_context_tokens)
|
|
304
|
+
?? toNullableInt(estimate?.explicitContextLimitTokens)
|
|
305
|
+
?? resolveContextLimitTokens(model, null, null, getModelContextLimitTokens);
|
|
306
|
+
const usedTotalTokens = toNullableInt(snapshot.used_total_tokens);
|
|
307
|
+
const usedPercent = toNullablePercent(snapshot.used_percent)
|
|
308
|
+
?? ((usedTotalTokens !== null && contextLimitTokens !== null && contextLimitTokens > 0)
|
|
309
|
+
? Math.round((usedTotalTokens / contextLimitTokens) * 10000) / 100
|
|
310
|
+
: null);
|
|
311
|
+
const freeTokens = toNullableInt(snapshot.free_tokens)
|
|
312
|
+
?? ((usedTotalTokens !== null && contextLimitTokens !== null)
|
|
313
|
+
? Math.max(0, contextLimitTokens - usedTotalTokens)
|
|
314
|
+
: null);
|
|
315
|
+
const completionTokens = toNullableInt(snapshot.used_completion_tokens)
|
|
316
|
+
?? toNullableInt(estimate?.totalCompletionTokens);
|
|
317
|
+
return {
|
|
318
|
+
...snapshot,
|
|
319
|
+
model,
|
|
320
|
+
max_context_tokens: contextLimitTokens,
|
|
321
|
+
used_percent: usedPercent,
|
|
322
|
+
free_tokens: freeTokens,
|
|
323
|
+
used_completion_tokens: completionTokens,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function consumeContextSignals(lines, options, seed = null) {
|
|
328
|
+
let latest = seed?.latest || null;
|
|
329
|
+
const estimate = createEstimateState(seed?.estimate || null);
|
|
330
|
+
for (const line of (lines || [])) {
|
|
331
|
+
if (!line) continue;
|
|
332
|
+
let event = null;
|
|
333
|
+
try {
|
|
334
|
+
event = JSON.parse(line);
|
|
335
|
+
} catch {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const data = event?.data && typeof event.data === 'object' ? event.data : null;
|
|
339
|
+
if (!data) continue;
|
|
340
|
+
updateEstimateState(estimate, event, data);
|
|
341
|
+
if (!hasSnapshotSignals(data)) continue;
|
|
342
|
+
latest = buildSnapshotFromEvent({ event, data, ...options });
|
|
343
|
+
}
|
|
344
|
+
return { latest, estimate };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function readFileChunk(fsModule, eventsPath, startOffset, endOffset) {
|
|
348
|
+
const safeStart = Math.max(0, Number(startOffset) || 0);
|
|
349
|
+
const safeEnd = Math.max(safeStart, Number(endOffset) || 0);
|
|
350
|
+
const byteLength = safeEnd - safeStart;
|
|
351
|
+
if (byteLength <= 0) return '';
|
|
352
|
+
|
|
353
|
+
const handle = fsModule.openSync(eventsPath, 'r');
|
|
354
|
+
try {
|
|
355
|
+
const buffer = Buffer.alloc(byteLength);
|
|
356
|
+
const bytesRead = fsModule.readSync(handle, buffer, 0, byteLength, safeStart);
|
|
357
|
+
return buffer.toString('utf8', 0, bytesRead);
|
|
358
|
+
} finally {
|
|
359
|
+
fsModule.closeSync(handle);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function splitJsonLines(text, carry = '') {
|
|
364
|
+
const combined = `${String(carry || '')}${String(text || '')}`;
|
|
365
|
+
if (!combined) return { lines: [], remainder: '' };
|
|
366
|
+
const normalized = combined.replace(/\r\n/g, '\n');
|
|
367
|
+
const endsWithNewline = normalized.endsWith('\n');
|
|
368
|
+
const parts = normalized.split('\n');
|
|
369
|
+
const remainder = endsWithNewline ? '' : parts.pop() || '';
|
|
370
|
+
return {
|
|
371
|
+
lines: parts.filter(Boolean),
|
|
372
|
+
remainder,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function createContextSnapshotService({
|
|
377
|
+
fs,
|
|
378
|
+
path,
|
|
379
|
+
resolveSessionStateRoot,
|
|
380
|
+
getModelContextLimitTokens = null,
|
|
381
|
+
} = {}) {
|
|
382
|
+
const contextCache = new Map();
|
|
383
|
+
|
|
384
|
+
function cacheRecord(cacheKey, record) {
|
|
385
|
+
contextCache.set(cacheKey, record);
|
|
386
|
+
if (contextCache.size <= CONTEXT_CACHE_MAX) return;
|
|
387
|
+
let oldestKey = null;
|
|
388
|
+
let oldestAt = Number.POSITIVE_INFINITY;
|
|
389
|
+
for (const [key, value] of contextCache.entries()) {
|
|
390
|
+
const cachedAt = Number(value?.cachedAt || 0);
|
|
391
|
+
if (cachedAt < oldestAt) {
|
|
392
|
+
oldestAt = cachedAt;
|
|
393
|
+
oldestKey = key;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (oldestKey) contextCache.delete(oldestKey);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function readContextFromSessionEvents(runtimeSessionId, runtimeSessionKey = null) {
|
|
400
|
+
const sessionId = normalizeText(runtimeSessionId);
|
|
401
|
+
const sessionKey = normalizeText(runtimeSessionKey || runtimeSessionId);
|
|
402
|
+
if (!sessionKey) {
|
|
403
|
+
return { snapshot: null, eventsPath: null, error: 'Missing runtime session ID' };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const root = String(resolveSessionStateRoot?.() || '').trim();
|
|
407
|
+
const eventsPath = path.join(root, sessionKey, 'events.jsonl');
|
|
408
|
+
if (!fs.existsSync(eventsPath)) {
|
|
409
|
+
contextCache.delete(sessionKey);
|
|
410
|
+
return { snapshot: null, eventsPath, error: `Session events file not found at ${eventsPath}` };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let stat = null;
|
|
414
|
+
try {
|
|
415
|
+
stat = fs.statSync(eventsPath);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
contextCache.delete(sessionKey);
|
|
418
|
+
return { snapshot: null, eventsPath, error: `Failed reading events file stats: ${error?.message || String(error)}` };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const sizeBytes = Number(stat?.size || 0);
|
|
422
|
+
const mtimeMs = Number(stat?.mtimeMs || 0);
|
|
423
|
+
const cached = contextCache.get(sessionKey) || null;
|
|
424
|
+
if (cached && cached.eventsPath === eventsPath && cached.sizeBytes === sizeBytes && cached.mtimeMs === mtimeMs) {
|
|
425
|
+
cached.cachedAt = Date.now();
|
|
426
|
+
return {
|
|
427
|
+
snapshot: hydrateSnapshotFromEstimate(cached.snapshot, cached.estimate, getModelContextLimitTokens),
|
|
428
|
+
eventsPath,
|
|
429
|
+
error: cached.error || null,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const canAppend = cached
|
|
434
|
+
&& cached.eventsPath === eventsPath
|
|
435
|
+
&& sizeBytes >= Number(cached.sizeBytes || 0);
|
|
436
|
+
const startOffset = canAppend ? Number(cached.sizeBytes || 0) : 0;
|
|
437
|
+
|
|
438
|
+
let chunkText = '';
|
|
439
|
+
try {
|
|
440
|
+
chunkText = readFileChunk(fs, eventsPath, startOffset, sizeBytes);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
return { snapshot: cached?.snapshot || null, eventsPath, error: `Failed reading events file: ${error?.message || String(error)}` };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const split = splitJsonLines(chunkText, canAppend ? cached?.remainder || '' : '');
|
|
446
|
+
const consumed = consumeContextSignals(split.lines, {
|
|
447
|
+
eventsPath,
|
|
448
|
+
sessionId,
|
|
449
|
+
pathModule: path,
|
|
450
|
+
getModelContextLimitTokens,
|
|
451
|
+
}, canAppend ? {
|
|
452
|
+
latest: cached?.latest || null,
|
|
453
|
+
estimate: cached?.estimate || null,
|
|
454
|
+
} : null);
|
|
455
|
+
|
|
456
|
+
const latest = consumed?.latest || null;
|
|
457
|
+
const estimated = buildEstimatedSnapshot({
|
|
458
|
+
estimate: consumed?.estimate || null,
|
|
459
|
+
eventsPath,
|
|
460
|
+
sessionId,
|
|
461
|
+
pathModule: path,
|
|
462
|
+
getModelContextLimitTokens,
|
|
463
|
+
});
|
|
464
|
+
const previousSnapshot = canAppend ? (cached?.snapshot || null) : null;
|
|
465
|
+
const previousError = canAppend ? (cached?.error || null) : null;
|
|
466
|
+
const nextSnapshot = hydrateSnapshotFromEstimate(
|
|
467
|
+
latest?.snapshot || estimated?.snapshot || previousSnapshot,
|
|
468
|
+
consumed?.estimate || null,
|
|
469
|
+
getModelContextLimitTokens,
|
|
470
|
+
);
|
|
471
|
+
const nextError = latest
|
|
472
|
+
? (latest.error || null)
|
|
473
|
+
: estimated
|
|
474
|
+
? (estimated.error || null)
|
|
475
|
+
: (previousError || (nextSnapshot ? null : 'No context-bearing events found for this session'));
|
|
476
|
+
|
|
477
|
+
cacheRecord(sessionKey, {
|
|
478
|
+
eventsPath,
|
|
479
|
+
sizeBytes,
|
|
480
|
+
mtimeMs,
|
|
481
|
+
latest,
|
|
482
|
+
estimate: consumed?.estimate || null,
|
|
483
|
+
snapshot: nextSnapshot,
|
|
484
|
+
error: nextError,
|
|
485
|
+
remainder: split.remainder,
|
|
486
|
+
cachedAt: Date.now(),
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
snapshot: nextSnapshot,
|
|
491
|
+
eventsPath,
|
|
492
|
+
error: nextError,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
readContextFromSessionEvents,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds the provider-neutral payload the context-usage modal renders.
|
|
5
|
+
*
|
|
6
|
+
* Claude sessions supply real per-category token counts from the Agent SDK;
|
|
7
|
+
* Copilot sessions only have the coarse system/tools/messages split recorded in
|
|
8
|
+
* their session events, so those are synthesized into the same shape. One
|
|
9
|
+
* renderer serves both.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Fallback ordering/colors for synthesized (Copilot) categories. Claude
|
|
13
|
+
// categories arrive with their own SDK-assigned colors.
|
|
14
|
+
const COPILOT_CATEGORY_COLORS = Object.freeze({
|
|
15
|
+
'System/Tools': 'blue',
|
|
16
|
+
Messages: 'orange',
|
|
17
|
+
Buffer: 'gray',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function toNullableInt(value) {
|
|
21
|
+
if (value === null || value === undefined || value === '') return null;
|
|
22
|
+
const n = Number(value);
|
|
23
|
+
if (!Number.isFinite(n)) return null;
|
|
24
|
+
return Math.round(n);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toNullablePercent(value) {
|
|
28
|
+
if (value === null || value === undefined || value === '') return null;
|
|
29
|
+
const n = Number(value);
|
|
30
|
+
if (!Number.isFinite(n)) return null;
|
|
31
|
+
return Math.round(n * 100) / 100;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeText(value) {
|
|
35
|
+
return String(value || '').trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function percentOf(tokens, maxTokens) {
|
|
39
|
+
if (tokens === null || maxTokens === null || maxTokens <= 0) return null;
|
|
40
|
+
return Math.round((tokens / maxTokens) * 10000) / 100;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Split the unused window into the part a conversation may actually occupy and
|
|
45
|
+
* the reserve above the auto-compact threshold.
|
|
46
|
+
*
|
|
47
|
+
* Every snapshot writer derives free space as `max - used`, which still
|
|
48
|
+
* *contains* the region the buffer describes. Anything rendering the two side
|
|
49
|
+
* by side (the usage grid, the modal's free row) needs disjoint slices, or the
|
|
50
|
+
* pair overshoots the window — latent until Claude began reporting a real
|
|
51
|
+
* buffer instead of a always-zero one.
|
|
52
|
+
*/
|
|
53
|
+
export function splitFreeAndBufferTokens(snapshot) {
|
|
54
|
+
const freeTokens = toNullableInt(snapshot?.free_tokens);
|
|
55
|
+
const bufferTokens = toNullableInt(snapshot?.buffer_tokens);
|
|
56
|
+
if (freeTokens === null || bufferTokens === null) return { freeTokens, bufferTokens };
|
|
57
|
+
// Only a writer that derived free space from `max - used` folded the buffer
|
|
58
|
+
// into it. A provider-reported remainder (Copilot's remainingTokens /
|
|
59
|
+
// availableTokens, which sit alongside its own safeBufferTokens) is already
|
|
60
|
+
// net of that reserve, and subtracting twice would understate it — so an
|
|
61
|
+
// unflagged snapshot is left alone.
|
|
62
|
+
if (snapshot?.free_tokens_includes_buffer !== true) return { freeTokens, bufferTokens };
|
|
63
|
+
// Past the threshold the reserve is bigger than what is left, so the buffer
|
|
64
|
+
// takes the remainder and free space reads as spent.
|
|
65
|
+
const reserved = Math.max(0, Math.min(bufferTokens, freeTokens));
|
|
66
|
+
return { freeTokens: freeTokens - reserved, bufferTokens: reserved };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildCopilotCategories(snapshot) {
|
|
70
|
+
// The auto-compact reserve is deliberately absent: it is not occupied
|
|
71
|
+
// context, and it now rides the view as its own field so Claude sessions
|
|
72
|
+
// (whose categories come from the SDK) account for it too.
|
|
73
|
+
const raw = [
|
|
74
|
+
['System/Tools', toNullableInt(snapshot?.system_tools_tokens)],
|
|
75
|
+
['Messages', toNullableInt(snapshot?.messages_tokens)],
|
|
76
|
+
];
|
|
77
|
+
return raw
|
|
78
|
+
.filter(([, tokens]) => tokens !== null && tokens > 0)
|
|
79
|
+
.map(([name, tokens]) => ({
|
|
80
|
+
name,
|
|
81
|
+
tokens,
|
|
82
|
+
color: COPILOT_CATEGORY_COLORS[name] || null,
|
|
83
|
+
isDeferred: false,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {object} args
|
|
89
|
+
* @param {object|null} args.snapshot the shared context snapshot contract
|
|
90
|
+
* @param {object|null} args.contextUsage normalized Claude SDK usage, when available
|
|
91
|
+
* @returns {object|null} null when there is nothing worth rendering
|
|
92
|
+
*/
|
|
93
|
+
export function buildContextUsageView({ snapshot = null, contextUsage = null } = {}) {
|
|
94
|
+
const maxTokens = toNullableInt(contextUsage?.maxTokens)
|
|
95
|
+
?? toNullableInt(snapshot?.max_context_tokens);
|
|
96
|
+
const totalTokens = toNullableInt(contextUsage?.totalTokens)
|
|
97
|
+
?? toNullableInt(snapshot?.used_total_tokens);
|
|
98
|
+
if (totalTokens === null && maxTokens === null) return null;
|
|
99
|
+
|
|
100
|
+
const percentage = toNullablePercent(contextUsage?.percentage)
|
|
101
|
+
?? toNullablePercent(snapshot?.used_percent)
|
|
102
|
+
?? percentOf(totalTokens, maxTokens);
|
|
103
|
+
|
|
104
|
+
// Free space and buffer are two slices of the same unused window, so they are
|
|
105
|
+
// resolved together before either is rendered.
|
|
106
|
+
const storedFreeTokens = toNullableInt(snapshot?.free_tokens);
|
|
107
|
+
const { freeTokens, bufferTokens } = splitFreeAndBufferTokens({
|
|
108
|
+
free_tokens: storedFreeTokens
|
|
109
|
+
?? ((totalTokens !== null && maxTokens !== null) ? Math.max(0, maxTokens - totalTokens) : null),
|
|
110
|
+
buffer_tokens: snapshot?.buffer_tokens,
|
|
111
|
+
// Deriving the value here means it necessarily contains the buffer.
|
|
112
|
+
free_tokens_includes_buffer: storedFreeTokens === null
|
|
113
|
+
? true
|
|
114
|
+
: snapshot?.free_tokens_includes_buffer === true,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const sourceCategories = Array.isArray(contextUsage?.categories) && contextUsage.categories.length
|
|
118
|
+
? contextUsage.categories
|
|
119
|
+
: buildCopilotCategories(snapshot);
|
|
120
|
+
|
|
121
|
+
const categories = sourceCategories
|
|
122
|
+
.map((entry) => ({
|
|
123
|
+
name: normalizeText(entry?.name),
|
|
124
|
+
tokens: toNullableInt(entry?.tokens),
|
|
125
|
+
color: normalizeText(entry?.color) || null,
|
|
126
|
+
isDeferred: entry?.isDeferred === true,
|
|
127
|
+
}))
|
|
128
|
+
.filter((entry) => entry.name && entry.tokens !== null && entry.tokens > 0)
|
|
129
|
+
.map((entry) => ({ ...entry, percent: percentOf(entry.tokens, maxTokens) }));
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
model: normalizeText(contextUsage?.model) || normalizeText(snapshot?.model) || null,
|
|
133
|
+
totalTokens,
|
|
134
|
+
maxTokens,
|
|
135
|
+
// Clamp: an estimated snapshot can report more than 100% (see the
|
|
136
|
+
// cumulative-output estimator), and the bar must not overflow.
|
|
137
|
+
percentage: percentage === null ? null : Math.max(0, Math.min(100, percentage)),
|
|
138
|
+
categories,
|
|
139
|
+
freeTokens,
|
|
140
|
+
freePercent: percentOf(freeTokens, maxTokens),
|
|
141
|
+
// Unused, but unusable: the window above the auto-compact threshold. Split
|
|
142
|
+
// out of free space, so the two are rendered as separate rows and the
|
|
143
|
+
// table still accounts for the whole window.
|
|
144
|
+
bufferTokens,
|
|
145
|
+
bufferPercent: percentOf(bufferTokens, maxTokens),
|
|
146
|
+
// A token count, not a percent — the CLI compacts once the conversation
|
|
147
|
+
// crosses it. `rawMaxTokens` rides along as recorded (it tracks the active
|
|
148
|
+
// window setting, NOT the model's own limit — probed 2026-08-20 — so
|
|
149
|
+
// nothing may annotate the slider from it).
|
|
150
|
+
autoCompactThreshold: toNullableInt(contextUsage?.autoCompactThreshold),
|
|
151
|
+
rawMaxTokens: toNullableInt(contextUsage?.rawMaxTokens),
|
|
152
|
+
autocompactSource: normalizeText(contextUsage?.autocompactSource) || null,
|
|
153
|
+
isAutoCompactEnabled: contextUsage?.isAutoCompactEnabled === true,
|
|
154
|
+
// Copilot's degraded path reports a lower bound only; the UI labels it
|
|
155
|
+
// rather than presenting it as measured.
|
|
156
|
+
isEstimate: !!normalizeText(snapshot?.estimate_kind),
|
|
157
|
+
estimateKind: normalizeText(snapshot?.estimate_kind) || null,
|
|
158
|
+
capturedAt: normalizeText(snapshot?.captured_at) || null,
|
|
159
|
+
};
|
|
160
|
+
}
|