thinkpool-pair 0.7.364 → 0.7.366

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.
Files changed (116) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +20 -0
  3. package/abort-turn-barrier.mjs +1 -23
  4. package/account.mjs +1 -1437
  5. package/acp-client.mjs +1 -140
  6. package/agent-detect.mjs +1 -28
  7. package/agent-notify.mjs +1 -142
  8. package/agent-visibility.mjs +1 -67
  9. package/auth-store.mjs +1 -120
  10. package/bridge.mjs +1 -6266
  11. package/byok-detect.mjs +1 -126
  12. package/claude-command-catalog.mjs +1 -91
  13. package/claude-session.mjs +1 -1519
  14. package/code-event-contract.mjs +1 -118
  15. package/codex-app-server.mjs +1 -340
  16. package/codex-commands.mjs +1 -83
  17. package/codex-event-mapper.mjs +1 -232
  18. package/codex-images.mjs +1 -69
  19. package/codex-mcp-http.mjs +1 -131
  20. package/codex-session.mjs +1 -1295
  21. package/command-catalog.mjs +1 -118
  22. package/command-guidance.mjs +1 -8
  23. package/context-contract.mjs +1 -95
  24. package/context-windows.mjs +1 -107
  25. package/cross-terminal.mjs +1 -789
  26. package/cumulative-event-relay.mjs +1 -53
  27. package/design-edit.mjs +1 -424
  28. package/design-source-contract.mjs +1 -4
  29. package/direct-pair-room.mjs +1 -57
  30. package/dispatch-lease.mjs +1 -37
  31. package/dispatch-permission-cleanup.mjs +1 -86
  32. package/edit-diff.mjs +1 -136
  33. package/error-recovery.mjs +1 -50
  34. package/event-bounds.mjs +1 -121
  35. package/event-delivery-queue.mjs +1 -60
  36. package/event-id.mjs +1 -549
  37. package/evidence-citations.mjs +1 -50
  38. package/evidence-compact.mjs +1 -11
  39. package/flow-assembly.mjs +1 -196
  40. package/flow-budget.mjs +1 -84
  41. package/flow-conductor.mjs +1 -259
  42. package/flow-context-store.mjs +1 -387
  43. package/flow-host-revert.mjs +1 -42
  44. package/flow-models.mjs +1 -139
  45. package/flow-preview.mjs +1 -148
  46. package/flow-receipt.mjs +1 -122
  47. package/flow-redispatch.mjs +1 -72
  48. package/flow-review-gate.mjs +1 -402
  49. package/flow-review-reflect.mjs +1 -115
  50. package/flow-review.mjs +1 -152
  51. package/flow-scope-evidence.mjs +1 -117
  52. package/flow-skill-registry.mjs +1 -140
  53. package/flow-task-graph.mjs +1 -562
  54. package/flow-worktree.mjs +1 -71
  55. package/git-diff-report.mjs +1 -121
  56. package/hermes-delegation-guard.mjs +1 -14
  57. package/hermes-event-mapper.mjs +1 -194
  58. package/hermes-isolation.mjs +1 -61
  59. package/hermes-model-cache.mjs +1 -54
  60. package/hermes-policy.mjs +1 -92
  61. package/hermes-probe.mjs +1 -57
  62. package/hermes-session.mjs +1 -683
  63. package/hermes-setup.mjs +1 -167
  64. package/host-memory.mjs +1 -116
  65. package/interrupted-resume.mjs +1 -95
  66. package/keep-awake.mjs +1 -148
  67. package/key-shape.mjs +1 -49
  68. package/lane-continuation.mjs +1 -83
  69. package/lane-lifecycle.mjs +1 -189
  70. package/lane-worktree.mjs +1 -77
  71. package/launcher.mjs +1 -354
  72. package/mcp-flight-recorder.mjs +1 -79
  73. package/mockup-delivery.mjs +1 -57
  74. package/model-prices.mjs +1 -113
  75. package/package.json +13 -4
  76. package/pair-bus.mjs +1 -98
  77. package/pair-control-authority.mjs +1 -89
  78. package/past-work-search.mjs +1 -105
  79. package/plan-meters.mjs +1 -144
  80. package/presence.mjs +1 -191
  81. package/privacy-report.mjs +1 -108
  82. package/provider-resilience.mjs +1 -356
  83. package/provider.mjs +1 -133
  84. package/providers.mjs +1 -491
  85. package/publish-guard.mjs +2 -0
  86. package/publish-manifest.json +129 -0
  87. package/question-response.mjs +1 -58
  88. package/reap-terminal.mjs +1 -68
  89. package/recap.mjs +1 -297
  90. package/replay-transport.mjs +1 -64
  91. package/repo-search.mjs +1 -190
  92. package/review-check.mjs +1 -182
  93. package/runtime-contract.mjs +1 -93
  94. package/runtime-registry.mjs +1 -64
  95. package/runtime-session.mjs +1 -20
  96. package/scheduled-run-admission.mjs +1 -364
  97. package/scheduled-runs.mjs +1 -268
  98. package/sdk-admission.mjs +1 -9
  99. package/sdk-smoke.mjs +1 -61
  100. package/serve-consent.mjs +1 -118
  101. package/serve-dir.mjs +1 -40
  102. package/service.mjs +1 -877
  103. package/session-store.mjs +1 -484
  104. package/side-lane.mjs +1 -63
  105. package/supabase-key.mjs +1 -176
  106. package/supervisor-ready.mjs +1 -57
  107. package/switch-provider.mjs +1 -84
  108. package/terminal-name.mjs +1 -359
  109. package/terminal-row-reconcile.mjs +1 -70
  110. package/thinkpool-prompt-contracts.mjs +1 -85
  111. package/thinkpool-room-prompt.mjs +1 -187
  112. package/transcript-sanitize.mjs +1 -332
  113. package/turn-stall.mjs +1 -61
  114. package/update-gate.mjs +1 -53
  115. package/viewport.mjs +1 -810
  116. package/worker-completion.mjs +1 -57
@@ -1,118 +1 @@
1
- // Runtime-neutral Code-room command catalog. Native runtimes publish very
2
- // different surfaces: Claude mixes skills with TUI commands, Codex App Server
3
- // publishes no slash catalog, and Hermes publishes ACP metadata. Keep the safe
4
- // room controls here so all three runtimes share names, descriptions, hints,
5
- // and routing while runtime-owned commands remain evidence-gated.
6
-
7
- const command = (name, description, route, inputHint, runtimes = ['claude', 'codex', 'hermes']) => Object.freeze({
8
- name,
9
- description,
10
- route,
11
- ...(inputHint ? { inputHint } : {}),
12
- runtimes: Object.freeze(runtimes),
13
- })
14
-
15
- export const CODE_ROOM_COMMANDS = Object.freeze([
16
- command('/side', 'investigate beside this terminal', 'side', 'task'),
17
- command('/help', 'list commands available in this lane', 'control'),
18
- command('/status', 'runtime, model, permissions, and busy state', 'control'),
19
- command('/usage', 'session usage and provider limits', 'control'),
20
- command('/context', 'current context-window usage', 'control'),
21
- command('/diff', 'Git changes and branch status', 'control'),
22
- command('/find', 'find relevant files in this repository', 'control', 'query'),
23
- command('/compact', 'compact context', 'runtime'),
24
- command('/clear', 'clear context · confirms', 'clear'),
25
- command('/model', 'pick model — opens selector', 'model'),
26
- command('/mode', 'set permission mode', 'mode', 'normal | auto-accept | plan | bypass'),
27
- command('/effort', 'set reasoning effort', 'effort', 'low | medium | high | xhigh | max'),
28
- command('/queue', 'run a prompt after the active turn', 'queue', 'prompt'),
29
- command('/steer', 'guide the active turn', 'steer', 'prompt', ['codex', 'hermes']),
30
- command('/credits', 'provider credit balance', 'credits', null, ['codex', 'hermes']),
31
- command('/review', 'review uncommitted changes', 'runtime', null, ['codex']),
32
- ])
33
-
34
- // Native catalogs are runtime evidence, never portable session metadata.
35
- // Claude's adapter default-denies raw SDK commands and passes only proven room
36
- // controls plus installed Skills. Hermes' isolated ACP bootstrap owns its safe
37
- // native catalog. Codex App Server publishes no native slash catalog, so a
38
- // restored/foreign list must not manufacture one. Compatibility aliases stay
39
- // callable at their runtime boundary without remaining discoverable.
40
- const NATIVE_COMMAND_POLICY = Object.freeze({
41
- claude: Object.freeze({ accept: true, hidden: Object.freeze(new Set(['/flow'])) }),
42
- codex: Object.freeze({ accept: false, hidden: Object.freeze(new Set(['/flow'])) }),
43
- hermes: Object.freeze({ accept: true, hidden: Object.freeze(new Set(['/flow', '/reasoning'])) }),
44
- })
45
-
46
- const cleanRuntime = (runtime) => runtime === 'thinkpool' ? 'hermes' : runtime
47
- const cleanName = (value) => {
48
- const raw = typeof value === 'string' ? value : value?.name
49
- const name = `/${String(raw || '').trim().replace(/^\/+/, '')}`
50
- return /^\/[A-Za-z0-9][A-Za-z0-9:_-]*$/.test(name) ? name : ''
51
- }
52
-
53
- const normalizeNative = (value) => {
54
- const name = cleanName(value)
55
- if (!name) return null
56
- if (typeof value === 'string') return { name, description: 'runtime command', route: 'runtime' }
57
- const description = String(value?.description || '').trim() || 'runtime command'
58
- const inputHint = String(value?.inputHint || value?.input_hint || value?.input?.hint || '').trim()
59
- const route = String(value?.route || '').trim() || 'runtime'
60
- return { name, description, route, ...(inputHint ? { inputHint } : {}) }
61
- }
62
-
63
- // Merge native metadata into the shared catalog without allowing a runtime to
64
- // redefine the routing of a bridge-owned control. Unknown native commands are
65
- // retained only after the runtime-specific adapter has already allowlisted them.
66
- export function commandCatalogForRuntime(runtime, nativeCommands = []) {
67
- const id = cleanRuntime(runtime)
68
- const nativePolicy = NATIVE_COMMAND_POLICY[id] || NATIVE_COMMAND_POLICY.codex
69
- const byName = new Map()
70
- for (const item of CODE_ROOM_COMMANDS) {
71
- if (!item.runtimes.includes(id)) continue
72
- byName.set(item.name, {
73
- name: item.name,
74
- description: item.description,
75
- route: item.route,
76
- ...(item.inputHint ? { inputHint: item.inputHint } : {}),
77
- })
78
- }
79
- for (const raw of (Array.isArray(nativeCommands) ? nativeCommands : [])) {
80
- const item = normalizeNative(raw)
81
- if (!item || !nativePolicy.accept || nativePolicy.hidden.has(item.name)) continue
82
- const shared = byName.get(item.name)
83
- const hermesNativeControl = id === 'hermes' && ['/help', '/status', '/context', '/credits'].includes(item.name)
84
- byName.set(item.name, shared
85
- ? { ...item, ...shared, description: item.description === 'runtime command' ? shared.description : item.description, ...(item.inputHint ? { inputHint: item.inputHint } : {}), ...(hermesNativeControl ? { route: 'runtime' } : {}) }
86
- : item)
87
- }
88
- return [...byName.values()]
89
- }
90
-
91
- export const commandNeedsInput = (item) => !!String(item?.inputHint || '').trim()
92
- export const commandRoute = (item) => String(item?.route || 'runtime')
93
-
94
- export function commandHelpLine(commands) {
95
- return (Array.isArray(commands) ? commands : [])
96
- .map((item) => `${item.name}${item.inputHint ? ` <${item.inputHint}>` : ''}`)
97
- .join(' ')
98
- }
99
-
100
- export function normalizePermissionMode(value) {
101
- const mode = String(value || '').trim().toLowerCase().replace(/[\s_-]+/g, '')
102
- if (['normal', 'default', 'manual'].includes(mode)) return 'default'
103
- if (['auto', 'autoaccept', 'autoacceptedits', 'acceptedits'].includes(mode)) return 'acceptEdits'
104
- if (mode === 'plan') return 'plan'
105
- if (['bypass', 'bypasspermissions', 'fullauto'].includes(mode)) return 'bypassPermissions'
106
- return null
107
- }
108
-
109
- // Command catalogs are session chrome, but their ACP system event is also the
110
- // lifecycle-ready signal. Keep those concerns separate so a reconstructed
111
- // identical catalog cannot swallow warm/recap/auto-resume work.
112
- export function reconcileCommandCatalog({ current, incoming, onChanged, onLifecycle } = {}) {
113
- const hasIncoming = Array.isArray(incoming) && incoming.length > 0
114
- const duplicate = hasIncoming && JSON.stringify(current || []) === JSON.stringify(incoming)
115
- if (hasIncoming && !duplicate) onChanged?.(incoming)
116
- onLifecycle?.({ duplicate, hasIncoming })
117
- return duplicate
118
- }
1
+ const e=(e,t,n,i,r=["claude","codex","hermes"])=>Object.freeze({name:e,description:t,route:n,...i?{inputHint:i}:{},runtimes:Object.freeze(r)});export const CODE_ROOM_COMMANDS=Object.freeze([e("/side","investigate beside this terminal","side","task"),e("/help","list commands available in this lane","control"),e("/status","runtime, model, permissions, and busy state","control"),e("/usage","session usage and provider limits","control"),e("/context","current context-window usage","control"),e("/diff","Git changes and branch status","control"),e("/find","find relevant files in this repository","control","query"),e("/compact","compact context","runtime"),e("/clear","clear context · confirms","clear"),e("/model","pick model — opens selector","model"),e("/mode","set permission mode","mode","normal | auto-accept | plan | bypass"),e("/effort","set reasoning effort","effort","low | medium | high | xhigh | max"),e("/queue","run a prompt after the active turn","queue","prompt"),e("/steer","guide the active turn","steer","prompt",["codex","hermes"]),e("/credits","provider credit balance","credits",null,["codex","hermes"]),e("/review","review uncommitted changes","runtime",null,["codex"])]);const t=Object.freeze({claude:Object.freeze({accept:!0,hidden:Object.freeze(new Set(["/flow"]))}),codex:Object.freeze({accept:!1,hidden:Object.freeze(new Set(["/flow"]))}),hermes:Object.freeze({accept:!0,hidden:Object.freeze(new Set(["/flow","/reasoning"]))})}),n=e=>{const t=(e=>{const t=`/${String(("string"==typeof e?e:e?.name)||"").trim().replace(/^\/+/,"")}`;return/^\/[A-Za-z0-9][A-Za-z0-9:_-]*$/.test(t)?t:""})(e);if(!t)return null;if("string"==typeof e)return{name:t,description:"runtime command",route:"runtime"};const n=String(e?.description||"").trim()||"runtime command",i=String(e?.inputHint||e?.input_hint||e?.input?.hint||"").trim();return{name:t,description:n,route:String(e?.route||"").trim()||"runtime",...i?{inputHint:i}:{}}};export function commandCatalogForRuntime(e,i=[]){const r=(e=>"thinkpool"===e?"hermes":e)(e),o=t[r]||t.codex,c=new Map;for(const e of CODE_ROOM_COMMANDS)e.runtimes.includes(r)&&c.set(e.name,{name:e.name,description:e.description,route:e.route,...e.inputHint?{inputHint:e.inputHint}:{}});for(const e of Array.isArray(i)?i:[]){const t=n(e);if(!t||!o.accept||o.hidden.has(t.name))continue;const i=c.get(t.name),s="hermes"===r&&["/help","/status","/context","/credits"].includes(t.name);c.set(t.name,i?{...t,...i,description:"runtime command"===t.description?i.description:t.description,...t.inputHint?{inputHint:t.inputHint}:{},...s?{route:"runtime"}:{}}:t)}return[...c.values()]}export const commandNeedsInput=e=>!!String(e?.inputHint||"").trim();export const commandRoute=e=>String(e?.route||"runtime");export function commandHelpLine(e){return(Array.isArray(e)?e:[]).map(e=>`${e.name}${e.inputHint?` <${e.inputHint}>`:""}`).join(" ")}export function normalizePermissionMode(e){const t=String(e||"").trim().toLowerCase().replace(/[\s_-]+/g,"");return["normal","default","manual"].includes(t)?"default":["auto","autoaccept","autoacceptedits","acceptedits"].includes(t)?"acceptEdits":"plan"===t?"plan":["bypass","bypasspermissions","fullauto"].includes(t)?"bypassPermissions":null}export function reconcileCommandCatalog({current:e,incoming:t,onChanged:n,onLifecycle:i}={}){const r=Array.isArray(t)&&t.length>0,o=r&&JSON.stringify(e||[])===JSON.stringify(t);return r&&!o&&n?.(t),i?.({duplicate:o,hasIncoming:r}),o}
@@ -1,8 +1 @@
1
- // Canonical, cache-busting invocation for every command printed to people.
2
- export const PAIR_CLI = 'npx thinkpool-pair@latest'
3
-
4
- export function pairCli(...args) {
5
- return [PAIR_CLI, ...args.filter((arg) => arg !== undefined && arg !== null && String(arg).length > 0)]
6
- .map(String)
7
- .join(' ')
8
- }
1
+ export const PAIR_CLI="npx thinkpool-pair@latest";export function pairCli(...t){return[PAIR_CLI,...t.filter(t=>null!=t&&String(t).length>0)].map(String).join(" ")}
@@ -1,95 +1 @@
1
- // Deterministic, local-only description of the safe context ThinkPool supplies
2
- // to a native runtime. This is deliberately not a transcript store.
3
-
4
- import { createHash } from 'node:crypto'
5
-
6
- export const CONTEXT_CONTRACT_VERSION = 1
7
- export const CONTEXT_OMISSIONS = Object.freeze(['raw_pty', 'credentials', 'absolute_paths', 'unapproved_cross_room'])
8
- export const CONTEXT_SOURCE_POLICY = Object.freeze([
9
- Object.freeze({ kind: 'current_user_turn', maxBytes: 8192 }),
10
- Object.freeze({ kind: 'room_now_delta', maxBytes: 4096 }),
11
- Object.freeze({ kind: 'flow_slice_digest', maxBytes: 8192 }),
12
- Object.freeze({ kind: 'approved_human_response', maxBytes: 4096 }),
13
- ])
14
-
15
- const BY_KIND = new Map(CONTEXT_SOURCE_POLICY.map((source) => [source.kind, source]))
16
- const HOST_PATH = /(?:\/home\/|\/users\/|\/private\/|\/tmp\/|[a-z]:\\|\\\\|(?:^|[\\/])\.\.(?:[\\/]|$))/i
17
- const SECRET_KEY = /(secret|token|password|authorization|api.?key|private.?key)/i
18
- const SECRET_VALUE = /(?:sk-[a-z0-9_-]{8,}|gsk_[a-z0-9_-]{8,}|AIza[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9]{20,}|github_pat_[a-z0-9_]{20,}|xox[baprs]-[a-z0-9-]{10,}|AKIA[0-9A-Z]{16}|sbp_[a-z0-9]{20,}|eyJ[a-z0-9_-]{16,}\.[a-z0-9_-]{16,}\.[a-z0-9_-]{8,}|bearer\s+[a-z0-9._-]{8,})/i
19
- const PROHIBITED = /(?:raw transcript|tool args?|chain[ -]of[ -]thought|hidden reasoning|system prompt|environment dump|provider key|BEGIN (?:RSA |OPENSSH )?PRIVATE KEY)/i
20
- const SAFE_REF = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,159}$/
21
- const bytes = (value) => {
22
- try {
23
- const serialized = typeof value === 'string' ? value : JSON.stringify(value)
24
- return new TextEncoder().encode(serialized ?? '').length
25
- } catch {
26
- return Infinity
27
- }
28
- }
29
- const stable = (value) => Array.isArray(value)
30
- ? value.map(stable)
31
- : value && typeof value === 'object'
32
- ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]))
33
- : value
34
-
35
- export function isSafeContextValue(value, maxBytes = 8192) {
36
- if (value == null || bytes(value) > maxBytes) return false
37
- const visit = (node) => {
38
- if (typeof node === 'string') return node.length <= 2048 && !HOST_PATH.test(node) && !SECRET_VALUE.test(node) && !PROHIBITED.test(node)
39
- if (node == null || typeof node === 'number' || typeof node === 'boolean') return true
40
- if (Array.isArray(node)) return node.length <= 64 && node.every(visit)
41
- if (typeof node !== 'object') return false
42
- return Object.entries(node).every(([key, child]) => !SECRET_KEY.test(key) && visit(child))
43
- }
44
- return visit(value)
45
- }
46
-
47
- function safeRef(ref) {
48
- if (!ref || typeof ref !== 'object' || Array.isArray(ref)) return null
49
- if (!SAFE_REF.test(String(ref.type || '')) || !SAFE_REF.test(String(ref.id || ''))) return null
50
- return { type: String(ref.type), id: String(ref.id) }
51
- }
52
-
53
- // The visible room receives only lane identity/status aggregates, never terminal
54
- // snippets or host worktree paths. This intentionally trades detail for privacy.
55
- export function projectRoomContext(roomNow) {
56
- const lines = String(roomNow || '').split('\n')
57
- const lanes = []
58
- let worktrees = 0
59
- for (const line of lines) {
60
- const lane = line.match(/^\s*-\s*([A-Za-z0-9._:@-]{4,160})\s+·\s+"([^"\n]{1,160})"\s+·\s+[^·\n]+\s+·\s+([^·\n]+)/)
61
- if (lane && isSafeContextValue(lane[2], 512) && isSafeContextValue(lane[3], 128)) {
62
- lanes.push({ ref: lane[1], title: lane[2].trim(), state: lane[3].trim() })
63
- continue
64
- }
65
- if (/^\s*-\s*\S+\s+[0-9a-f]{7,40}\s+\[/.test(line)) worktrees++
66
- }
67
- const projection = { lanes: lanes.slice(0, 32), worktreeCount: Math.min(worktrees, 999) }
68
- return isSafeContextValue(projection, 4096) ? projection : { lanes: [], worktreeCount: 0 }
69
- }
70
-
71
- export function formatRoomContextProjection(roomNow) {
72
- const projection = projectRoomContext(roomNow)
73
- if (!projection.lanes.length && !projection.worktreeCount) return ''
74
- const laneText = projection.lanes.map((lane) => `${lane.ref} · ${lane.title} · ${lane.state}`).join('\n')
75
- return ['ROOM NOW (safe projection):', laneText, projection.worktreeCount ? `Active git worktrees: ${projection.worktreeCount}` : ''].filter(Boolean).join('\n')
76
- }
77
-
78
- export function buildContextManifest({ promptBundle = {}, sources = [] } = {}) {
79
- const accepted = new Map()
80
- for (const candidate of Array.isArray(sources) ? sources : []) {
81
- const policy = BY_KIND.get(candidate?.kind)
82
- if (!policy || accepted.has(policy.kind)) continue
83
- const ref = candidate.ref == null ? null : safeRef(candidate.ref)
84
- if (candidate.ref != null && !ref) continue
85
- if (candidate.value != null && !isSafeContextValue(candidate.value, policy.maxBytes)) continue
86
- accepted.set(policy.kind, ref ? { kind: policy.kind, ref, maxBytes: policy.maxBytes } : { kind: policy.kind, maxBytes: policy.maxBytes })
87
- }
88
- const manifest = {
89
- version: CONTEXT_CONTRACT_VERSION,
90
- promptBundle: { version: Number(promptBundle?.version) || 1, hash: /^[a-f0-9]{64}$/i.test(String(promptBundle?.hash || '')) ? String(promptBundle.hash).toLowerCase() : null },
91
- sources: CONTEXT_SOURCE_POLICY.map((policy) => accepted.get(policy.kind)).filter(Boolean),
92
- omissions: [...CONTEXT_OMISSIONS],
93
- }
94
- return Object.freeze({ ...manifest, hash: createHash('sha256').update(JSON.stringify(stable(manifest))).digest('hex') })
95
- }
1
+ import{createHash as e}from"node:crypto";export const CONTEXT_CONTRACT_VERSION=1;export const CONTEXT_OMISSIONS=Object.freeze(["raw_pty","credentials","absolute_paths","unapproved_cross_room"]);export const CONTEXT_SOURCE_POLICY=Object.freeze([Object.freeze({kind:"current_user_turn",maxBytes:8192}),Object.freeze({kind:"room_now_delta",maxBytes:4096}),Object.freeze({kind:"flow_slice_digest",maxBytes:8192}),Object.freeze({kind:"approved_human_response",maxBytes:4096})]);const t=new Map(CONTEXT_SOURCE_POLICY.map(e=>[e.kind,e])),n=/(?:\/home\/|\/users\/|\/private\/|\/tmp\/|[a-z]:\\|\\\\|(?:^|[\\/])\.\.(?:[\\/]|$))/i,r=/(secret|token|password|authorization|api.?key|private.?key)/i,o=/(?:sk-[a-z0-9_-]{8,}|gsk_[a-z0-9_-]{8,}|AIza[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9]{20,}|github_pat_[a-z0-9_]{20,}|xox[baprs]-[a-z0-9-]{10,}|AKIA[0-9A-Z]{16}|sbp_[a-z0-9]{20,}|eyJ[a-z0-9_-]{16,}\.[a-z0-9_-]{16,}\.[a-z0-9_-]{8,}|bearer\s+[a-z0-9._-]{8,})/i,s=/(?:raw transcript|tool args?|chain[ -]of[ -]thought|hidden reasoning|system prompt|environment dump|provider key|BEGIN (?:RSA |OPENSSH )?PRIVATE KEY)/i,a=/^[A-Za-z0-9][A-Za-z0-9._:@-]{0,159}$/,i=e=>Array.isArray(e)?e.map(i):e&&"object"==typeof e?Object.fromEntries(Object.keys(e).sort().map(t=>[t,i(e[t])])):e;export function isSafeContextValue(e,t=8192){if(null==e||(e=>{try{const t="string"==typeof e?e:JSON.stringify(e);return(new TextEncoder).encode(t??"").length}catch{return 1/0}})(e)>t)return!1;const a=e=>"string"==typeof e?e.length<=2048&&!n.test(e)&&!o.test(e)&&!s.test(e):null==e||"number"==typeof e||"boolean"==typeof e||(Array.isArray(e)?e.length<=64&&e.every(a):"object"==typeof e&&Object.entries(e).every(([e,t])=>!r.test(e)&&a(t)));return a(e)}function p(e){return!e||"object"!=typeof e||Array.isArray(e)?null:a.test(String(e.type||""))&&a.test(String(e.id||""))?{type:String(e.type),id:String(e.id)}:null}export function projectRoomContext(e){const t=String(e||"").split("\n"),n=[];let r=0;for(const e of t){const t=e.match(/^\s*-\s*([A-Za-z0-9._:@-]{4,160})\s+·\s+"([^"\n]{1,160})"\s+·\s+[^·\n]+\s+·\s+([^·\n]+)/);t&&isSafeContextValue(t[2],512)&&isSafeContextValue(t[3],128)?n.push({ref:t[1],title:t[2].trim(),state:t[3].trim()}):/^\s*-\s*\S+\s+[0-9a-f]{7,40}\s+\[/.test(e)&&r++}const o={lanes:n.slice(0,32),worktreeCount:Math.min(r,999)};return isSafeContextValue(o,4096)?o:{lanes:[],worktreeCount:0}}export function formatRoomContextProjection(e){const t=projectRoomContext(e);return t.lanes.length||t.worktreeCount?["ROOM NOW (safe projection):",t.lanes.map(e=>`${e.ref} · ${e.title} · ${e.state}`).join("\n"),t.worktreeCount?`Active git worktrees: ${t.worktreeCount}`:""].filter(Boolean).join("\n"):""}export function buildContextManifest({promptBundle:n={},sources:r=[]}={}){const o=new Map;for(const e of Array.isArray(r)?r:[]){const n=t.get(e?.kind);if(!n||o.has(n.kind))continue;const r=null==e.ref?null:p(e.ref);(null==e.ref||r)&&(null==e.value||isSafeContextValue(e.value,n.maxBytes))&&o.set(n.kind,r?{kind:n.kind,ref:r,maxBytes:n.maxBytes}:{kind:n.kind,maxBytes:n.maxBytes})}const s={version:1,promptBundle:{version:Number(n?.version)||1,hash:/^[a-f0-9]{64}$/i.test(String(n?.hash||""))?String(n.hash).toLowerCase():null},sources:CONTEXT_SOURCE_POLICY.map(e=>o.get(e.kind)).filter(Boolean),omissions:[...CONTEXT_OMISSIONS]};return Object.freeze({...s,hash:e("sha256").update(JSON.stringify(i(s))).digest("hex")})}
@@ -1,107 +1 @@
1
- /* ─────────────────────────────────────────────────────────────
2
- context-windows — correct the /code context meter for non-Claude BYOK models.
3
-
4
- The Claude Agent SDK computes its context-usage meter
5
- (getContextUsage → { totalTokens, maxTokens, percentage }) from the model
6
- STRING it believes it is talking to. When a user bridges a NON-Claude model
7
- in over an Anthropic-compatible endpoint (ANTHROPIC_BASE_URL — GLM, DeepSeek,
8
- Kimi, Qwen, … via OpenRouter or a provider's own compat endpoint), the SDK's
9
- maxTokens is for the WRONG model, so the meter fills at the wrong rate and can
10
- shoot past 100% (Max, 2026-07-04: GLM-4.6 "runs up to 100 and goes past pretty
11
- fast").
12
-
13
- This is a DISPLAY-ONLY correction applied on the bridge's EMIT path: we
14
- override the emitted { max, pct, over } using the real window for the reported
15
- model id. The SDK's own context management is untouched — no behavior change,
16
- no racy control request (H42-safe by construction).
17
-
18
- Numbers are the provider-HEADLINED context windows (verified per row below).
19
- The meter is inherently approximate, so we match what each provider's own docs
20
- advertise rather than power-of-two token counts.
21
- ───────────────────────────────────────────────────────────── */
22
-
23
- // id substring (case-insensitive) → context window in tokens.
24
- // Ordered MOST-SPECIFIC first (glm-4.6 before glm-4.5) — first hit wins.
25
- // Sources verified 2026-07-04:
26
- // glm-4.6 200K — https://openrouter.ai/z-ai/glm-4.6 · https://docs.z.ai
27
- // glm-4.5 128K — https://github.com/zai-org/GLM-4.5 (README)
28
- // deepseek v3.x 128K — https://api-docs.deepseek.com (deepseek-chat/-reasoner, V3.1)
29
- // kimi-k2 256K — https://huggingface.co/moonshotai/Kimi-K2-Instruct-0905
30
- // qwen3-coder 256K — https://huggingface.co/Qwen/Qwen3-Coder-Next (native)
31
- const KNOWN_WINDOWS = [
32
- { match: 'glm-4.6', max: 200_000 },
33
- { match: 'glm-4.5', max: 128_000 },
34
- { match: 'deepseek', max: 128_000 },
35
- { match: 'kimi-k2', max: 256_000 },
36
- { match: 'qwen3-coder', max: 256_000 },
37
- ]
38
-
39
- // A Claude model id — the ONLY ids whose SDK-reported window we trust verbatim.
40
- // Everything else is a candidate for correction. (Kept broad on purpose: any
41
- // real Claude id matches, so the 1M/200k Claude paths never regress.)
42
- export function isClaudeModel(id) {
43
- return /claude|anthropic|opus|sonnet|haiku/i.test(String(id || ''))
44
- }
45
-
46
- // Look up a corrected window (tokens) for a model id. Returns null if unknown.
47
- export function windowForModel(modelId) {
48
- const id = String(modelId || '').toLowerCase()
49
- if (!id) return null
50
- for (const w of KNOWN_WINDOWS) {
51
- if (id.includes(w.match)) return w.max
52
- }
53
- return null
54
- }
55
-
56
- // Parse TP_CONTEXT_MAX (a positive integer token count) from the environment.
57
- // Per-bridge override for models not in the map. null if unset/non-numeric/<=0.
58
- export function envContextMax(env = process.env) {
59
- const raw = env?.TP_CONTEXT_MAX
60
- if (raw == null || raw === '') return null
61
- const n = Number(raw)
62
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : null
63
- }
64
-
65
- // Correct a raw ctx object from the SDK's getContextUsage().
66
- //
67
- // raw: { used, max, pct, model } (any field may be missing)
68
- // env: process.env-like (for the TP_CONTEXT_MAX override)
69
- //
70
- // Returns a NEW ctx object. Precedence:
71
- // 1. TP_CONTEXT_MAX override — wins for ANY model (incl. ids not in the map).
72
- // 2. A known non-Claude window from the map.
73
- // 3. Otherwise passthrough — Claude models and unknown ids keep the SDK's max.
74
- // When a correction applies, max is replaced, pct is recomputed and clamped to
75
- // 0..100, and `over: true` is set when used exceeds the corrected window.
76
- // In every case pct is clamped so a lying >100% never reaches the UI.
77
- export function correctContext(raw, env = process.env) {
78
- if (!raw || typeof raw !== 'object') return raw
79
- const used = Number(raw.used)
80
- const override = envContextMax(env)
81
- // The map fires only for non-Claude ids (point 4: Claude untouched); the
82
- // env override is explicit and deliberate, so it applies even to Claude.
83
- const mapped = override == null && !isClaudeModel(raw.model)
84
- ? windowForModel(raw.model)
85
- : null
86
- const correctedMax = override ?? mapped
87
-
88
- if (correctedMax == null) {
89
- // No correction available. For a NON-Claude model the SDK's max is computed for the
90
- // wrong model id (it believes it's talking to Claude over ANTHROPIC_BASE_URL) → a %
91
- // off that wrong max misleads (the GLM-ran-past-100 bug, 2026-07-04). Rather than ship
92
- // a wrong number, SUPPRESS the meter: return null so the mode row + Session info show
93
- // nothing / "—" instead. We only display ctx when we know the true window — a Claude
94
- // id (SDK max trusted), a known-map model (corrected above), or TP_CONTEXT_MAX (override).
95
- if (!isClaudeModel(raw.model)) return null
96
- const pct = Number(raw.pct)
97
- if (Number.isFinite(pct) && pct > 100) return { ...raw, pct: 100, over: true }
98
- return raw
99
- }
100
-
101
- const max = correctedMax
102
- const pct = Number.isFinite(used) && max > 0
103
- ? Math.min(100, Math.max(0, Math.round((used / max) * 100)))
104
- : raw.pct
105
- const over = Number.isFinite(used) && used > max
106
- return { ...raw, max, pct, over }
107
- }
1
+ const e=[{match:"glm-4.6",max:2e5},{match:"glm-4.5",max:128e3},{match:"deepseek",max:128e3},{match:"kimi-k2",max:256e3},{match:"qwen3-coder",max:256e3}];export function isClaudeModel(e){return/claude|anthropic|opus|sonnet|haiku/i.test(String(e||""))}export function windowForModel(t){const n=String(t||"").toLowerCase();if(!n)return null;for(const t of e)if(n.includes(t.match))return t.max;return null}export function envContextMax(e=process.env){const t=e?.TP_CONTEXT_MAX;if(null==t||""===t)return null;const n=Number(t);return Number.isFinite(n)&&n>0?Math.floor(n):null}export function correctContext(e,t=process.env){if(!e||"object"!=typeof e)return e;const n=Number(e.used),o=envContextMax(t),r=null!=o||isClaudeModel(e.model)?null:windowForModel(e.model),u=o??r;if(null==u){if(!isClaudeModel(e.model))return null;const t=Number(e.pct);return Number.isFinite(t)&&t>100?{...e,pct:100,over:!0}:e}const i=u,l=Number.isFinite(n)&&i>0?Math.min(100,Math.max(0,Math.round(n/i*100))):e.pct,c=Number.isFinite(n)&&n>i;return{...e,max:i,pct:l,over:c}}