clauderipple 0.9.4 → 0.10.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/CHANGELOG.md +80 -0
- package/README.md +31 -0
- package/dist/app/package.json +1 -1
- package/dist/cli/src/hooks/agent-title.js +7 -4
- package/dist/cli/src/index.js +43 -0
- package/dist/cli/src/remote.js +263 -0
- package/dist/cli/src/schtasks.js +48 -40
- package/dist/cli/src/settings.js +2 -1
- package/dist/mod/hooks/i18n.ts +252 -6
- package/dist/mod/hooks/judge.ts +35 -17
- package/dist/mod/hooks/modes.ts +56 -1
- package/dist/mod/hooks/register.tsx +238 -32
- package/dist/mod/hooks/session.ts +98 -1
- package/dist/mod/hooks/settings.js +7 -3
- package/dist/mod/hooks/settings.ts +9 -4
- package/dist/mod/hooks/stats.ts +25 -1
- package/dist/mod/hooks/ui/agents.tsx +858 -79
- package/dist/mod/hooks/ui/alerts.tsx +20 -17
- package/dist/mod/hooks/ui/art.ts +33 -7
- package/dist/mod/hooks/ui/band.tsx +262 -81
- package/dist/mod/hooks/ui/flat.ts +265 -0
- package/dist/mod/hooks/ui/kit.tsx +70 -55
- package/dist/mod/hooks/ui/look.ts +129 -0
- package/dist/mod/hooks/ui/settings.tsx +71 -46
- package/dist/mod/hooks/ui/setup.tsx +15 -13
- package/dist/mod/hooks/ui/theme.ts +3 -0
- package/dist/mod/hooks/ui/tools.ts +124 -0
- package/dist/mod/hooks/ui/view.ts +116 -3
- package/dist/mod/hooks/workers.ts +33 -6
- package/dist/mod/tests/register.test.tsx +155 -11
- package/dist/mod/tests/ui.test.tsx +375 -27
- package/dist/mod/types/index.d.ts +68 -2
- package/dist/remote/package.json +28 -0
- package/dist/remote/src/away.js +85 -0
- package/dist/remote/src/bridge-client.js +295 -0
- package/dist/remote/src/composer-menu.js +11 -0
- package/dist/remote/src/config.js +102 -0
- package/dist/remote/src/desktop.js +1018 -0
- package/dist/remote/src/e2ee.js +126 -0
- package/dist/remote/src/e2ee.ts +126 -0
- package/dist/remote/src/eventlog.js +298 -0
- package/dist/remote/src/expo-push.js +127 -0
- package/dist/remote/src/folders.js +79 -0
- package/dist/remote/src/fsutil.js +39 -0
- package/dist/remote/src/handoff.js +38 -0
- package/dist/remote/src/host-service.js +168 -0
- package/dist/remote/src/images.js +215 -0
- package/dist/remote/src/local-control.js +138 -0
- package/dist/remote/src/main.js +264 -0
- package/dist/remote/src/permissions.js +124 -0
- package/dist/remote/src/power.js +76 -0
- package/dist/remote/src/processes.js +24 -0
- package/dist/remote/src/protocol.js +32 -0
- package/dist/remote/src/protocol.ts +284 -0
- package/dist/remote/src/push-content.js +32 -0
- package/dist/remote/src/push.js +134 -0
- package/dist/remote/src/receipts.js +111 -0
- package/dist/remote/src/relay.js +438 -0
- package/dist/remote/src/router.js +208 -0
- package/dist/remote/src/sdk-launcher.js +18 -0
- package/dist/remote/src/server.js +706 -0
- package/dist/remote/src/service.js +157 -0
- package/dist/remote/src/sessions.js +1800 -0
- package/dist/remote/src/sidebar-storage.js +211 -0
- package/dist/remote/src/sidebar.js +173 -0
- package/dist/remote/src/tool-details.js +205 -0
- package/dist/remote/src/tool-details.ts +153 -0
- package/dist/remote/src/worktree.js +99 -0
- package/dist/remote/web/app.ts +746 -0
- package/dist/remote/web/conn.ts +193 -0
- package/dist/remote/web/demo.ts +134 -0
- package/dist/remote/web/dom.ts +124 -0
- package/dist/remote/web/esc.ts +4 -0
- package/dist/remote/web/i18n.ts +374 -0
- package/dist/remote/web/icon.svg +1 -0
- package/dist/remote/web/index.html +24 -0
- package/dist/remote/web/manifest.webmanifest +10 -0
- package/dist/remote/web/mascot.ts +60 -0
- package/dist/remote/web/md.ts +78 -0
- package/dist/remote/web/notify.ts +55 -0
- package/dist/remote/web/pushstate.ts +27 -0
- package/dist/remote/web/secure.ts +109 -0
- package/dist/remote/web/sidebar.ts +106 -0
- package/dist/remote/web/style.css +311 -0
- package/dist/remote/web/sw.ts +48 -0
- package/dist/remote/web/timeline.ts +224 -0
- package/dist/remote/web/views.ts +323 -0
- package/dist/router/src/admin.js +227 -4
- package/dist/router/src/config.js +30 -0
- package/dist/router/src/index.js +39 -0
- package/dist/router/src/ingress/translate.js +6 -2
- package/dist/router/src/phone.js +82 -0
- package/dist/router/src/presets.js +3 -0
- package/dist/router/src/providers/anthropic-usage.js +33 -1
- package/dist/router/src/providers/chatgpt/index.js +41 -11
- package/dist/router/src/providers/chatgpt/translate.js +147 -7
- package/dist/router/src/providers/google/index.js +26 -0
- package/dist/router/src/providers/google/prompt-refusal.js +29 -0
- package/dist/router/src/providers/google/usage.js +257 -0
- package/dist/router/src/providers/grok/index.js +18 -0
- package/dist/router/src/providers/grok/usage.js +77 -0
- package/dist/router/src/providers/openai/index.js +29 -5
- package/dist/router/src/providers/openai/translate.js +260 -27
- package/dist/router/src/providers/opencode-usage.js +116 -0
- package/dist/router/src/proxy.js +339 -258
- package/dist/router/src/reasoning.js +157 -0
- package/dist/router/src/remote-host.js +320 -0
- package/dist/router/src/remote-tunnel.js +320 -0
- package/dist/router/src/requestlog.js +48 -39
- package/dist/router/src/thinking.js +20 -9
- package/dist/router/src/usage-history.js +227 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +436 -59
- package/dist/ui/i18n.js +184 -0
- package/dist/ui/index.html +24 -5
- package/dist/ui/style.css +39 -0
- package/docs/ARCHITECTURE.md +164 -12
- package/package.json +7 -1
package/dist/mod/hooks/modes.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Operating modes in the session (docs/specs/2026-10-12-worker-tiers.md "Operating modes"): which
|
|
2
2
|
// mode is on, when it ends by itself, and the eager-delegation note. Pure; register.tsx wires them.
|
|
3
3
|
|
|
4
|
-
import type { Hot, Mode, Settings } from '../types'
|
|
4
|
+
import type { Hot, LimitItem, Mode, Settings } from '../types'
|
|
5
5
|
|
|
6
6
|
/** The mode that is on, if the settings name one that exists. */
|
|
7
7
|
export function activeModeOf(s: Pick<Settings, 'modes' | 'activeMode'>): Mode | undefined {
|
|
@@ -91,3 +91,58 @@ export function modeOver(mode: Pick<Mode, 'until'>, since: number | undefined, n
|
|
|
91
91
|
const passed = (at: number) => at > since && at <= now
|
|
92
92
|
return windows.some(w => passed(w.at) || (w.windowMs !== undefined && passed(w.at - w.windowMs)))
|
|
93
93
|
}
|
|
94
|
+
|
|
95
|
+
/** The configured providers of ChatGPT type, from GET /api/status `providers` (name → {type}). */
|
|
96
|
+
export function chatgptProviders(status: unknown): string[] {
|
|
97
|
+
const providers = record(status).providers
|
|
98
|
+
if (!providers || typeof providers !== 'object' || Array.isArray(providers)) return []
|
|
99
|
+
return Object.entries(providers as Record<string, unknown>)
|
|
100
|
+
.filter(([, value]) => record(value).type === 'chatgpt')
|
|
101
|
+
.map(([name]) => name)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Every signed-in ChatGPT account's known windows as the limits row reads them, from GET
|
|
106
|
+
* /api/chatgpt-accounts (`quota.rate_limits.{primary,secondary}` with `reset_at` in epoch
|
|
107
|
+
* seconds or ms, or `reset_after_seconds` from the snapshot's `at`, and `window_minutes`;
|
|
108
|
+
* the same shape `quotaWindows` reads). Paused and needs-login accounts are left out (and take
|
|
109
|
+
* no number), and so is an account with no snapshot. `account` numbers the accounts kept, in
|
|
110
|
+
* answer order. A snapshot is not aged out: the router takes it from the account's own replies,
|
|
111
|
+
* so an account idle for an hour holds an hour-old figure that is still right (measured
|
|
112
|
+
* 2026-10-15: 30 minutes old between uses). Only a window whose reset has passed is dropped,
|
|
113
|
+
* since its figure no longer stands.
|
|
114
|
+
*/
|
|
115
|
+
export function gptLimitsFrom(body: unknown, provider: string, now: number): LimitItem[] {
|
|
116
|
+
const accounts = record(body).accounts
|
|
117
|
+
if (!Array.isArray(accounts)) return []
|
|
118
|
+
const kept = accounts.map(record).filter(account => account.state !== 'paused' && account.state !== 'needs-login' && account.paused !== true)
|
|
119
|
+
const out: LimitItem[] = []
|
|
120
|
+
kept.forEach((account, index) => {
|
|
121
|
+
const quota = record(account.quota)
|
|
122
|
+
const at = quota.at
|
|
123
|
+
if (typeof at !== 'number') return
|
|
124
|
+
const limits = record(quota.rate_limits)
|
|
125
|
+
for (const key of ['primary', 'secondary']) {
|
|
126
|
+
const window = record(limits[key])
|
|
127
|
+
const used = window.used_percent
|
|
128
|
+
if (typeof used !== 'number' || !Number.isFinite(used)) continue
|
|
129
|
+
const minutes = window.window_minutes
|
|
130
|
+
const resetAt = typeof window.reset_at === 'number' ? (window.reset_at > 1e12 ? window.reset_at : window.reset_at * 1000)
|
|
131
|
+
: typeof window.reset_after_seconds === 'number' && typeof at === 'number' ? at + window.reset_after_seconds * 1000 : NaN
|
|
132
|
+
if (Number.isFinite(resetAt) && resetAt <= now) continue
|
|
133
|
+
const kind = minutes === 300 ? 'five_hour' : minutes === 10080 ? 'seven_day' : 'window'
|
|
134
|
+
out.push({
|
|
135
|
+
provider,
|
|
136
|
+
type: 'chatgpt',
|
|
137
|
+
title: 'GPT',
|
|
138
|
+
account: index,
|
|
139
|
+
kind,
|
|
140
|
+
label: '',
|
|
141
|
+
...(typeof minutes === 'number' && minutes > 0 ? { windowMinutes: minutes } : {}),
|
|
142
|
+
percent: Math.round(used * 10) / 10,
|
|
143
|
+
...(Number.isFinite(resetAt) ? { resetsAt: new Date(resetAt).toISOString() } : {}),
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
return out
|
|
148
|
+
}
|
|
@@ -2,29 +2,32 @@
|
|
|
2
2
|
// Timers own background work; hooks only admit/rewrite a request and record its result.
|
|
3
3
|
import { atom, read, update } from 'claude-code'
|
|
4
4
|
import type { AgentSpawnInput, EngineInterface, ModelCompleteResult, ModelForkResult, Register, RenderElement, RenderInput, SessionRateLimit, ToolCallInput, TurnStepInput, TurnUsage } from 'claude-code'
|
|
5
|
-
import type { AgentRec, Effort, JudgeInput, JudgeKind, JudgeRun, ModSettingsSnapshot, Pick, Settings, StatusRequest, TierTarget, Verdict, WorkerCandidate, WorkerPick, WorkerVerdict } from '../types'
|
|
6
|
-
import { MOD_SETTINGS_SCHEMA_VERSION, applySettingsChanges, normalizeSettings, settingsChanges } from './settings'
|
|
5
|
+
import type { AgentRec, Effort, JudgeInput, JudgeKind, JudgeRun, LimitItem, ModSettingsSnapshot, Pick, Settings, StatusRequest, TierTarget, Verdict, WorkerCandidate, WorkerPick, WorkerVerdict } from '../types'
|
|
6
|
+
import { MOD_SETTINGS_SCHEMA_VERSION, RUNG_EFFORT, applySettingsChanges, normalizeSettings, settingsChanges } from './settings'
|
|
7
|
+
import { rungOf } from './ladder'
|
|
7
8
|
import { debugText, langFrom, t } from './i18n'
|
|
8
9
|
import type { Lang } from './i18n'
|
|
9
10
|
import { EFFORT_JUDGE_SYSTEM, finalEffort, isCacheSafeModel, isShortFollowUp, jevEffortRequest, jevFailure, jevUsage, jevWorkerRequest, judgeContext, parseEffortVerdict, parseJevEffort, parseJevKey, parseJevWorker, shouldJudge } from './judge'
|
|
10
|
-
import { AUTO_DESCRIPTION, WORKER_JUDGE_SYSTEM, agentProvider, candidateUsable, chatgptUsable, claudeCandidates, ladderOptionsFrom, ladderPick, parseWorkerVerdict, readMarker, routerModelOptions, spawnModel, spawnTitle, takesOver, workerJudgeInput, zenJev } from './workers'
|
|
11
|
+
import { AUTO_DESCRIPTION, WORKER_JUDGE_SYSTEM, agentProvider, candidateUsable, chatgptUsable, claudeCandidates, ladderOptionsFrom, ladderPick, parseWorkerVerdict, readMarker, routerModelOptions, spawnModel, spawnTitle, takesOver, TIER_MARKERS, tierOf, workerJudgeInput, zenJev } from './workers'
|
|
11
12
|
import { MARKER, agentModel, fixSpawn } from './worker'
|
|
12
|
-
import { activeModeOf, claudeWindow, eagerNote, modeKey, modeOver, quotaWindows, resetProvider, routingSettings } from './modes'
|
|
13
|
+
import { activeModeOf, chatgptProviders, claudeWindow, eagerNote, gptLimitsFrom, modeKey, modeOver, quotaWindows, resetProvider, routingSettings } from './modes'
|
|
13
14
|
import type { QuotaWindow } from './modes'
|
|
14
15
|
import type { Spawn, WorkerFix } from './worker'
|
|
15
16
|
import { afterPing, nextMemo, restoreFromResume, shouldPing } from './cache'
|
|
16
|
-
import { bandPriority, claudeLimitsFrom, coldAlert, hotAlert, hottest, keptAlert, saveUntil as limitSaveUntil, swampAlert, swamped } from './session'
|
|
17
|
+
import { bandPriority, claudeLimitsFrom, claudeScopedFrom, coldAlert, hotAlert, hottest, hotWindowMoved, keptAlert, routerLimitItems, saveUntil as limitSaveUntil, swampAlert, swamped } from './session'
|
|
18
|
+
import type { ScopedLimit } from './session'
|
|
17
19
|
import { COMPACT_SYSTEM, compactResult, serializeTranscript } from './compact'
|
|
18
20
|
import { ADVICE_SYSTEM, FULL_PROMPT, QUICK_PROMPT, adviceDue, adviceInput, parseAdvice, wrapForNewChat } from './handoff'
|
|
19
|
-
import { addJudge, addPrompt, addRequest, addWarm, addWorker, emptyStats, formatStats,
|
|
21
|
+
import { addAgentStep, addJudge, addPrompt, addRequest, addWarm, addWorker, emptyStats, formatStats, sentEffort } from './stats'
|
|
20
22
|
import { besideBand, renderBand, renderFooter } from './ui/band'
|
|
21
23
|
import { renderAlert } from './ui/alerts'
|
|
22
24
|
import { renderSettings } from './ui/settings'
|
|
23
25
|
import { renderSetup } from './ui/setup'
|
|
24
|
-
import { renderAgents } from './ui/agents'
|
|
26
|
+
import { renderAgents, talkOf } from './ui/agents'
|
|
27
|
+
import { toolDetail } from './ui/tools'
|
|
25
28
|
import { renderLog } from './ui/log'
|
|
26
29
|
import { EFFORTS, modelName, nextMinuteChange, routeOf, targetLabel } from './ui/view'
|
|
27
|
-
import type { AlertActions, AlertView, BandActions, DashboardView, Frame, SettingsActions, SettingsView, SetupActions } from './ui/view'
|
|
30
|
+
import type { AgentTalk, AgentsActions, AlertActions, AlertView, BandActions, DashboardView, Frame, SettingsActions, SettingsView, SetupActions } from './ui/view'
|
|
28
31
|
|
|
29
32
|
const line = atom({ plugin: 'ripple', key: 'line' } as const, null)
|
|
30
33
|
const last = atom({ plugin: 'ripple', key: 'last' } as const, null)
|
|
@@ -53,7 +56,9 @@ const lastRealReplyAt = atom({ plugin: 'ripple', key: 'lastRealReplyAt' } as con
|
|
|
53
56
|
const context = atom({ plugin: 'ripple', key: 'context' } as const, null)
|
|
54
57
|
const hot = atom({ plugin: 'ripple', key: 'hot' } as const, null)
|
|
55
58
|
const limit = atom({ plugin: 'ripple', key: 'limit' } as const, null)
|
|
59
|
+
const limits = atom({ plugin: 'ripple', key: 'limits' } as const, null)
|
|
56
60
|
const hotHidden = atom({ plugin: 'ripple', key: 'hotHidden' } as const, null)
|
|
61
|
+
const hotHiddenFor = atom({ plugin: 'ripple', key: 'hotHiddenFor' } as const, null)
|
|
57
62
|
const saveUntil = atom({ plugin: 'ripple', key: 'saveUntil' } as const, null)
|
|
58
63
|
const swampHidden = atom({ plugin: 'ripple', key: 'swampHidden' } as const, null)
|
|
59
64
|
const coldHidden = atom({ plugin: 'ripple', key: 'coldHidden' } as const, false)
|
|
@@ -82,6 +87,10 @@ const compactNote = atom({ plugin: 'ripple', key: 'compactNote' } as const, '')
|
|
|
82
87
|
const effortMenuOpen = atom({ plugin: 'ripple', key: 'effortMenuOpen' } as const, false)
|
|
83
88
|
const agentsOpen = atom({ plugin: 'ripple', key: 'agentsOpen' } as const, false)
|
|
84
89
|
const agentsShowDone = atom({ plugin: 'ripple', key: 'agentsShowDone' } as const, false)
|
|
90
|
+
const agentOpen = atom({ plugin: 'ripple', key: 'agentOpen' } as const, null)
|
|
91
|
+
const agentEarlier = atom({ plugin: 'ripple', key: 'agentEarlier' } as const, 0)
|
|
92
|
+
const agentRunsOpen = atom({ plugin: 'ripple', key: 'agentRunsOpen' } as const, [])
|
|
93
|
+
const agentsTick = atom({ plugin: 'ripple', key: 'agentsTick' } as const, 0)
|
|
85
94
|
const jev = atom({ plugin: 'ripple', key: 'jev' } as const, { source: null, test: { state: 'idle' } })
|
|
86
95
|
const routerModels = atom({ plugin: 'ripple', key: 'routerModels' } as const, [])
|
|
87
96
|
const ladderOptions = atom({ plugin: 'ripple', key: 'ladderOptions' } as const, null)
|
|
@@ -144,12 +153,18 @@ async function rippleHome($: EngineInterface): Promise<string> {
|
|
|
144
153
|
}
|
|
145
154
|
// The fs API has no append: serialize read/write pairs so concurrent picks in this mod survive.
|
|
146
155
|
let workerLogWork: Promise<void> = Promise.resolve()
|
|
156
|
+
/**
|
|
157
|
+
* Lines the pick log keeps. Each delegation writes up to three (pick, shadow verdict, outcome), so
|
|
158
|
+
* 300 lines held only a few days; weighing the judge against the main chat's tier markers needs
|
|
159
|
+
* weeks (2026-10-15).
|
|
160
|
+
*/
|
|
161
|
+
const PICK_LOG_LINES = 3000
|
|
147
162
|
async function persistWorkerPick($: EngineInterface, entry: Record<string, unknown>): Promise<void> {
|
|
148
163
|
const work = workerLogWork.then(async () => {
|
|
149
164
|
try {
|
|
150
165
|
const path = `${await rippleHome($)}/logs/worker-picks.jsonl`
|
|
151
166
|
const old = await $.fs.exists(path) ? await $.fs.read(path) : ''
|
|
152
|
-
const lines = old.split('\n').filter(line => line.trim()).slice(-
|
|
167
|
+
const lines = old.split('\n').filter(line => line.trim()).slice(-(PICK_LOG_LINES - 1))
|
|
153
168
|
await $.fs.write(path, [...lines, JSON.stringify(entry)].join('\n') + '\n')
|
|
154
169
|
} catch (error) { await report($, 'worker pick log', error) }
|
|
155
170
|
})
|
|
@@ -346,7 +361,15 @@ async function toggleLog($: EngineInterface): Promise<boolean> {
|
|
|
346
361
|
const language = await read($, lang)
|
|
347
362
|
const open = await isLogUp($)
|
|
348
363
|
if (open) await $.ui.close({ id: LOG_PANE })
|
|
349
|
-
else
|
|
364
|
+
else {
|
|
365
|
+
// The surface may decline to draw the pane; say why rather than doing nothing on a press.
|
|
366
|
+
const opened = await $.ui.open({ id: LOG_PANE, title: t(language, 'commands.logTitle') })
|
|
367
|
+
if (!opened.isPlaced) {
|
|
368
|
+
const why = 'reason' in opened && opened.reason ? String(opened.reason) : 'not placed'
|
|
369
|
+
$.ui.toast(t(language, 'toasts.logNotPlaced', { reason: why }))
|
|
370
|
+
await report($, 'log pane', why)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
350
373
|
await syncPanes($)
|
|
351
374
|
if (!open) await refreshRouter($)
|
|
352
375
|
return !open
|
|
@@ -412,6 +435,18 @@ async function workerFiles($: EngineInterface): Promise<Map<string, string>> {
|
|
|
412
435
|
}
|
|
413
436
|
return files
|
|
414
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* The provider a started agent runs on, for its card's dot: its worker file's (`via <provider>.`),
|
|
440
|
+
* Anthropic for a Claude id, else the router's catalogue entry for the id.
|
|
441
|
+
*/
|
|
442
|
+
async function spawnProvider($: EngineInterface, subagentType: string, model: string): Promise<string | undefined> {
|
|
443
|
+
const file = (await workerFiles($)).get(subagentType)
|
|
444
|
+
const fromFile = file ? agentProvider(file) : undefined
|
|
445
|
+
if (fromFile) return fromFile
|
|
446
|
+
const id = model.replace(/@[^@]*$/, '')
|
|
447
|
+
if (/^(?:sonnet|opus|haiku|fable|mythos|claude-)/.test(id)) return 'anthropic'
|
|
448
|
+
return (await read($, routerModels)).find(m => m.value === id)?.provider
|
|
449
|
+
}
|
|
415
450
|
async function workerFix($: EngineInterface, spawn: Spawn): Promise<WorkerFix | null> {
|
|
416
451
|
const text = await optionalFile($, `${await rippleHome($)}/generated-agents.json`)
|
|
417
452
|
if (!text || !strings(object(JSON.parse(text)).agents).includes(spawn.subagentType)) return null
|
|
@@ -660,7 +695,9 @@ async function requestPick($: EngineInterface, e: AgentSpawnInput, s: Settings):
|
|
|
660
695
|
const pinnedCandidates = [...[...files].flatMap(([agent, file]) => { const model = agentModel(file)?.replace(/@[^@]*$/, ''); const provider = agentProvider(file); return model && provider ? [{ target: { agent }, model, provider }] : [] }), ...claudeCandidates()]
|
|
661
696
|
const usable = await availability($)
|
|
662
697
|
const marker = readMarker(e.prompt)
|
|
663
|
-
const
|
|
698
|
+
const tier = tierOf(marker)
|
|
699
|
+
const explicit = marker && !tier && !['auto', 'ripple'].includes(marker.name) ? marker : null
|
|
700
|
+
const id = (await $.clock.now()).toString(36) + Math.random().toString(36).slice(2, 8)
|
|
664
701
|
let verdict: WorkerVerdict = { domain: 'general', level: 'standard', effort: 'high', why: t(language, 'toasts.judgeUnavailable') }
|
|
665
702
|
let by: WorkerPick['by'] = 'default'
|
|
666
703
|
let ms = 0
|
|
@@ -673,6 +710,13 @@ async function requestPick($: EngineInterface, e: AgentSpawnInput, s: Settings):
|
|
|
673
710
|
target = name ? { agent: name } : { agent: 'general-purpose', model: explicit.name }
|
|
674
711
|
verdict = { domain: 'general', level: 'standard', effort: effortOf(explicit.level) ?? 'high', why: t(language, 'toasts.explicitMarker') }
|
|
675
712
|
by = 'marker'
|
|
713
|
+
} else if (tier) {
|
|
714
|
+
// The main chat named the tier. The judge still rates the same brief, unawaited, so the log
|
|
715
|
+
// can later say which of the two was right (see shadowJudge).
|
|
716
|
+
const named = TIER_MARKERS[tier]!
|
|
717
|
+
verdict = { domain: 'general', ...named, effort: RUNG_EFFORT[rungOf(named)], why: t(language, 'toasts.sessionTier', { tier }) }
|
|
718
|
+
by = 'session'
|
|
719
|
+
void shadowJudge($, s, id, { description: e.description, prompt: e.prompt })
|
|
676
720
|
} else {
|
|
677
721
|
const judged = await judge($, s, { description: e.description, prompt: e.prompt }, true)
|
|
678
722
|
if (judged.verdict && 'domain' in judged.verdict) { rawVerdict = judged.verdict; verdict = judged.verdict; by = judged.by }
|
|
@@ -700,18 +744,34 @@ async function requestPick($: EngineInterface, e: AgentSpawnInput, s: Settings):
|
|
|
700
744
|
const model = spawnModel(selected, verdict.effort, files.has(selected.agent) ? agentModel(files.get(selected.agent)!) : undefined)
|
|
701
745
|
let prompt = e.prompt
|
|
702
746
|
if (marker) prompt = prompt.replace(MARKER, '').replace(/^\s*\n/, '')
|
|
703
|
-
const value: WorkerPick = { ...verdict, ...resolved, target: selected, by, ms, ...(fallbackReason ? { fallbackReason } : {}) }
|
|
747
|
+
const value: WorkerPick = { ...verdict, ...resolved, target: selected, by, ms, id, ...(fallbackReason ? { fallbackReason } : {}) }
|
|
704
748
|
// The verdict behind a pick is otherwise only in the agents panel; one debug line per pick keeps
|
|
705
749
|
// it checkable: what the judge said, the rung it landed on and what ran.
|
|
706
750
|
const entry = { task: e.description.slice(0, 200), domain: value.domain, level: value.level, kind: value.kind ?? 'do', sure: value.sure, rung: value.rung, position: value.position, skipped: value.skipped, target: value.target, model: model ?? null, effort: value.effort, by, ...(mode ? { mode: mode.name } : {}), ...(fallbackReason ? { fallbackReason } : {}) }
|
|
707
751
|
await $.ui.log('ClaudeRipple worker pick: ' + JSON.stringify(entry), { to: 'debug' })
|
|
708
|
-
|
|
752
|
+
// The brief itself is never logged (it can hold anything); its length is. To re-judge a pick,
|
|
753
|
+
// its brief is in the session's own transcript under the same description.
|
|
754
|
+
const named = by === 'marker' || by === 'default' || by === 'session'
|
|
755
|
+
await persistWorkerPick($, { type: 'pick', id, ...entry, at: new Date(now).toISOString(), verdict: rawVerdict, by: named ? by : 'judge', judge: named ? null : by, ...(tier ? { tier } : {}), ms, ...(judgeError ? { error: judgeError } : {}), promptChars: e.prompt.length })
|
|
709
756
|
if (fallbackReason) await $.ui.toast(t(language, 'toasts.workerFallback', { reason: fallbackReason, name: targetLabel(selected, language) }))
|
|
710
757
|
await update($, lastSpawn, () => value)
|
|
711
758
|
await update($, stats, current => addWorker(current, verdict.level))
|
|
712
759
|
return { input: { ...e, subagentType: selected.agent, model, prompt }, pick: value }
|
|
713
760
|
}
|
|
714
761
|
|
|
762
|
+
/**
|
|
763
|
+
* What the judge would have said of a brief the main chat already tiered: logged beside the pick
|
|
764
|
+
* (same `id`), never awaited by the spawn, never acted on. Together with each pick's outcome it is
|
|
765
|
+
* what decides later whether the judge stays.
|
|
766
|
+
*/
|
|
767
|
+
async function shadowJudge($: EngineInterface, s: Settings, id: string, input: { description: string; prompt: string }): Promise<void> {
|
|
768
|
+
try {
|
|
769
|
+
const judged = await judge($, s, input, true)
|
|
770
|
+
const verdict = judged.verdict && 'domain' in judged.verdict ? judged.verdict : null
|
|
771
|
+
await persistWorkerPick($, { type: 'shadow', pick: id, at: new Date(await $.clock.now()).toISOString(), verdict, rung: verdict ? rungOf(verdict) : null, judge: judged.by, ms: judged.ms, ...(judged.error ? { error: judged.error } : {}) })
|
|
772
|
+
} catch (error) { await report($, 'shadow judge', error) }
|
|
773
|
+
}
|
|
774
|
+
|
|
715
775
|
async function toggleAuto($: EngineInterface): Promise<void> {
|
|
716
776
|
const s = await currentSettings($)
|
|
717
777
|
await saveSettings($, { ...s, effortAuto: !s.effortAuto })
|
|
@@ -941,6 +1001,81 @@ async function routerClaudeLimits($: EngineInterface): Promise<SessionRateLimit[
|
|
|
941
1001
|
return status.ok ? claudeLimitsFrom(JSON.parse(status.text), await $.clock.now()) : []
|
|
942
1002
|
} catch (error) { await report($, 'claude limits', error); return [] }
|
|
943
1003
|
}
|
|
1004
|
+
/** The router's figures for the Claude login (5-hour, weekly, per-model weeklies), at most one
|
|
1005
|
+
* status lookup a minute and only while the limits row can show them; a failure keeps the last
|
|
1006
|
+
* good numbers rather than blanking them. */
|
|
1007
|
+
let routerClaudeAt = 0
|
|
1008
|
+
let routerClaudeCache: { limits: SessionRateLimit[]; scoped: ScopedLimit[] } = { limits: [], scoped: [] }
|
|
1009
|
+
async function routerClaude($: EngineInterface): Promise<{ limits: SessionRateLimit[]; scoped: ScopedLimit[] }> {
|
|
1010
|
+
const now = await $.clock.now()
|
|
1011
|
+
if (!(await currentSettings($)).extras.includes('limits') || now - routerClaudeAt < 60_000) return routerClaudeCache
|
|
1012
|
+
routerClaudeAt = now
|
|
1013
|
+
try {
|
|
1014
|
+
const status = await $.http.fetch(`${await adminUrl($)}/api/status`)
|
|
1015
|
+
if (status.ok) {
|
|
1016
|
+
const body: unknown = JSON.parse(status.text)
|
|
1017
|
+
routerClaudeCache = { limits: claudeLimitsFrom(body, now), scoped: claudeScopedFrom(body, now) }
|
|
1018
|
+
}
|
|
1019
|
+
} catch (error) { await report($, 'claude limits row', error) }
|
|
1020
|
+
return routerClaudeCache
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* The limits row as a router that predates `GET /api/limits` still answers it: Claude's plan in the
|
|
1024
|
+
* unified shape, from the older `/api/status` and `/api/chatgpt-accounts` routes. Kept so the row
|
|
1025
|
+
* shows exactly as before until the router is restarted onto the new endpoint.
|
|
1026
|
+
*/
|
|
1027
|
+
async function fallbackLimits($: EngineInterface): Promise<LimitItem[]> {
|
|
1028
|
+
const now = await $.clock.now()
|
|
1029
|
+
const fromRouter = await routerClaude($)
|
|
1030
|
+
const claudeChat = /^claude-/.test(await $.session.model())
|
|
1031
|
+
// The 5-hour and the weekly (one each, the hotter reading winning when both sources name it), then
|
|
1032
|
+
// the per-model weeklies the router reports. Claude's plan shows whatever this chat runs on.
|
|
1033
|
+
const usage = claudeChat ? (await $.session.usage()).rateLimits : []
|
|
1034
|
+
const mains = new Map<'five_hour' | 'seven_day', SessionRateLimit>()
|
|
1035
|
+
for (const w of [...usage, ...fromRouter.limits]) {
|
|
1036
|
+
if (w.kind !== 'five_hour' && w.kind !== 'seven_day') continue
|
|
1037
|
+
const prev = mains.get(w.kind)
|
|
1038
|
+
if (!prev || w.percentUsed > prev.percentUsed) mains.set(w.kind, w)
|
|
1039
|
+
}
|
|
1040
|
+
const items: LimitItem[] = (['five_hour', 'seven_day'] as const)
|
|
1041
|
+
.flatMap(kind => mains.get(kind) ? [mains.get(kind)!] : [])
|
|
1042
|
+
.map(w => ({ provider: 'claude', type: 'anthropic', title: 'Claude', account: 0, kind: w.kind as 'five_hour' | 'seven_day', label: '', percent: w.percentUsed, ...(w.resetsAt ? { resetsAt: w.resetsAt } : {}) }))
|
|
1043
|
+
for (const s of fromRouter.scoped) items.push({ provider: 'claude', type: 'anthropic', title: 'Claude', account: 0, kind: 'scoped', label: s.name, percent: s.percent, ...(s.resetsAt ? { resetsAt: s.resetsAt } : {}) })
|
|
1044
|
+
const admin = await adminUrl($)
|
|
1045
|
+
const status = await $.http.fetch(`${admin}/api/status`)
|
|
1046
|
+
if (status.ok) for (const provider of chatgptProviders(JSON.parse(status.text))) {
|
|
1047
|
+
const response = await $.http.fetch(`${admin}/api/chatgpt-accounts?provider=${encodeURIComponent(provider)}`)
|
|
1048
|
+
if (!response.ok) continue
|
|
1049
|
+
const body: unknown = JSON.parse(response.text)
|
|
1050
|
+
// The router answers for its first ChatGPT provider when the name is not one: not this provider.
|
|
1051
|
+
if (object(body).provider !== provider) continue
|
|
1052
|
+
items.push(...gptLimitsFrom(body, provider, now))
|
|
1053
|
+
}
|
|
1054
|
+
return items
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* The whole limits row from the router's `GET /api/limits` — every provider whose quota it can read,
|
|
1058
|
+
* Claude first — at most one round a minute and only while the row can show it. On a router that
|
|
1059
|
+
* predates the endpoint (404) the older per-provider routes answer instead, so the row shows before
|
|
1060
|
+
* the router is restarted. A failure keeps the last good row rather than blanking it, and the state
|
|
1061
|
+
* is written only when it moved: a redraw that shows nothing new is wasted (each answer is drawn
|
|
1062
|
+
* afresh).
|
|
1063
|
+
*/
|
|
1064
|
+
let limitsAt = 0
|
|
1065
|
+
async function refreshLimits($: EngineInterface): Promise<void> {
|
|
1066
|
+
if (!(await currentSettings($)).extras.includes('limits')) return
|
|
1067
|
+
const now = await $.clock.now()
|
|
1068
|
+
if (now - limitsAt < 60_000) return
|
|
1069
|
+
limitsAt = now
|
|
1070
|
+
try {
|
|
1071
|
+
const admin = await adminUrl($)
|
|
1072
|
+
const response = await $.http.fetch(`${admin}/api/limits`)
|
|
1073
|
+
let items: LimitItem[] | null = null
|
|
1074
|
+
if (response.ok) items = routerLimitItems(JSON.parse(response.text), now)
|
|
1075
|
+
else if (response.status === 404) items = await fallbackLimits($)
|
|
1076
|
+
if (items && !same(items, await read($, limits))) await update($, limits, () => items)
|
|
1077
|
+
} catch (error) { await report($, 'limits', error) }
|
|
1078
|
+
}
|
|
944
1079
|
async function refreshUsage($: EngineInterface): Promise<void> {
|
|
945
1080
|
const usage = await $.session.usage()
|
|
946
1081
|
const c = usage.context
|
|
@@ -956,10 +1091,18 @@ async function refreshUsage($: EngineInterface): Promise<void> {
|
|
|
956
1091
|
const rateLimits = own || !/^claude-/.test(await $.session.model()) ? usage.rateLimits : [...usage.rateLimits, ...await routerClaudeLimits($)]
|
|
957
1092
|
const h = hottest(rateLimits)
|
|
958
1093
|
const previous = await read($, hot)
|
|
959
|
-
|
|
1094
|
+
// A put-off lasts until the window it was put off for moves on (session.ts hotWindowMoved). A
|
|
1095
|
+
// put-off from an older mod carries no window: it adopts the current one.
|
|
1096
|
+
if (await read($, hotHidden) !== null) {
|
|
1097
|
+
const putOffFor = await read($, hotHiddenFor)
|
|
1098
|
+
if (!putOffFor) { if (h) await update($, hotHiddenFor, () => h) }
|
|
1099
|
+
else if (hotWindowMoved(putOffFor, h)) { await update($, hotHidden, () => null); await update($, hotHiddenFor, () => null) }
|
|
1100
|
+
}
|
|
960
1101
|
if (JSON.stringify(h) !== JSON.stringify(previous)) await update($, hot, () => h)
|
|
961
1102
|
const top = rateLimits.filter(l => ['five_hour', 'seven_day'].includes(l.kind)).sort((a, b) => b.percentUsed - a.percentUsed)[0]
|
|
962
1103
|
const l = top ? { kind: top.kind, percent: top.percentUsed, ...(top.resetsAt ? { resetsAt: top.resetsAt } : {}) } : null
|
|
1104
|
+
// The limits row itself is read from the router's `/api/limits` (refreshLimits); the host's own
|
|
1105
|
+
// windows still bind the first row's limit and the hot alert here, as they always have.
|
|
963
1106
|
// Check the previous binding deadline before a rolled-forward snapshot replaces it.
|
|
964
1107
|
await endModeIfOver($)
|
|
965
1108
|
if (JSON.stringify(l) !== JSON.stringify(await read($, limit))) await update($, limit, () => l)
|
|
@@ -997,6 +1140,7 @@ async function tick($: EngineInterface): Promise<void> {
|
|
|
997
1140
|
ticking = true
|
|
998
1141
|
try {
|
|
999
1142
|
await refreshUsage($)
|
|
1143
|
+
await refreshLimits($)
|
|
1000
1144
|
const now = await $.clock.now()
|
|
1001
1145
|
const list = await read($, agents)
|
|
1002
1146
|
if (list.some(a => a.state === 'running' && a.toolSince !== undefined && now - a.toolSince >= 8000)) {
|
|
@@ -1044,6 +1188,18 @@ function startTimers($: EngineInterface): void {
|
|
|
1044
1188
|
$.clock.after(1, async () => { await refreshRouter($); await tick($) })
|
|
1045
1189
|
$.clock.every(3000, async () => { await refreshRouter($) })
|
|
1046
1190
|
$.clock.every(15000, async () => { await tick($) })
|
|
1191
|
+
$.clock.every(1000, async () => { await agentsClock($) })
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* The Agents pane's clocks: each second an agent runs while the pane is open, one write that only
|
|
1195
|
+
* the pane reads, so its elapsed times tick and nothing else redraws.
|
|
1196
|
+
*/
|
|
1197
|
+
async function agentsClock($: EngineInterface): Promise<void> {
|
|
1198
|
+
try {
|
|
1199
|
+
if (!(await read($, agents)).some(a => a.state === 'running' || a.state === 'waiting')) return
|
|
1200
|
+
if (!(await $.ui.panes()).some(p => p.id === AGENTS_PANE)) return
|
|
1201
|
+
await update($, agentsTick, n => n + 1)
|
|
1202
|
+
} catch (error) { await report($, 'agents clock', error) }
|
|
1047
1203
|
}
|
|
1048
1204
|
|
|
1049
1205
|
async function dashboard($: EngineInterface, working: boolean): Promise<DashboardView> {
|
|
@@ -1061,6 +1217,7 @@ async function dashboard($: EngineInterface, working: boolean): Promise<Dashboar
|
|
|
1061
1217
|
model: await read($, modelIs) ? modelName((await read($, modelIs))!) : null,
|
|
1062
1218
|
context: await read($, context), cache: await read($, cache), warm: { ...w, on: await keepWarmOn($, s), ...(await read($, warmHere) !== null ? { here: true } : {}) },
|
|
1063
1219
|
limit: l ? { ...l, save: saving } : null, route: record ? routeOf(record) : null, notice: await read($, line),
|
|
1220
|
+
limits: await read($, limits) ?? [],
|
|
1064
1221
|
auto: s.effortAuto, handoff: await read($, advice) ? 'advised' : s.handoffButton === 'always' ? 'always' : 'off',
|
|
1065
1222
|
agents: { live: list.filter(a => ['running', 'waiting'].includes(a.state)).length, total: list.length },
|
|
1066
1223
|
logOpen: await read($, logOpen), agentsOpen: await read($, agentsOpen), effortMenu: await read($, effortMenuOpen),
|
|
@@ -1075,7 +1232,7 @@ async function frameOf($: EngineInterface, e: RenderInput): Promise<Frame> {
|
|
|
1075
1232
|
const s = await currentSettings($)
|
|
1076
1233
|
const columns = 'bodyColumns' in e.props ? e.props.bodyColumns : e.viewport?.columns ?? 100
|
|
1077
1234
|
const maxRows = 'maxRows' in e.props ? e.props.maxRows : 'scroll' in e.props ? e.props.scroll.bodyRows : 1
|
|
1078
|
-
return { surface: e.surface, columns, maxRows, now: await $.clock.now(), appearance: s.appearance, lang: await read($, lang) }
|
|
1235
|
+
return { surface: e.surface, columns, maxRows, now: await $.clock.now(), appearance: s.appearance, skin: s.skin, lang: await read($, lang) }
|
|
1079
1236
|
}
|
|
1080
1237
|
async function alertView($: EngineInterface, now: number): Promise<AlertView | null> {
|
|
1081
1238
|
const language = await read($, lang)
|
|
@@ -1122,8 +1279,11 @@ async function markSeen($: EngineInterface): Promise<void> {
|
|
|
1122
1279
|
}
|
|
1123
1280
|
async function dismiss($: EngineInterface, view: AlertView): Promise<void> {
|
|
1124
1281
|
await update($, demo, () => null)
|
|
1125
|
-
if (view.kind === 'hot')
|
|
1126
|
-
|
|
1282
|
+
if (view.kind === 'hot') {
|
|
1283
|
+
const current = await read($, hot)
|
|
1284
|
+
await update($, hotHidden, () => view.percent)
|
|
1285
|
+
await update($, hotHiddenFor, () => current)
|
|
1286
|
+
} else if (view.kind === 'cold') await update($, coldHidden, () => true)
|
|
1127
1287
|
else if (view.kind === 'kept') await update($, kept, () => null)
|
|
1128
1288
|
else if (view.kind === 'swamp') await update($, swampHidden, () => view.percent)
|
|
1129
1289
|
else if (view.kind === 'judgeDown') await update($, judgeDownHidden, () => view.reason)
|
|
@@ -1154,10 +1314,33 @@ function alertActions($: EngineInterface, view: AlertView): AlertActions {
|
|
|
1154
1314
|
return { compact: note => runCompact($, note), clear: async () => { await update($, panel, () => null); await $.command.run({ command: 'clear', args: '' }) }, handoff: () => openHandoff($), dismiss: () => dismiss($, view), toggleSave: () => toggleSave($), openSettings: () => startPanel($, 'settings'),
|
|
1155
1315
|
noteInput: text => update($, compactNote, () => text), setHandoffKind: kind => update($, handoffChoiceKind, () => kind), setHandoffAfter: after => update($, handoffChoiceAfter, () => after), go: () => queueHandoff($), copy: () => copyHandoff($), openNew: () => moveToNewSession($) }
|
|
1156
1316
|
}
|
|
1157
|
-
function
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1317
|
+
function agentsActions($: EngineInterface): AgentsActions {
|
|
1318
|
+
return {
|
|
1319
|
+
toggleDone: () => update($, agentsShowDone, value => !value),
|
|
1320
|
+
open: async id => { await update($, agentEarlier, () => 0); await update($, agentRunsOpen, () => []); await update($, agentOpen, () => id) },
|
|
1321
|
+
// Back on the list, the runs unfolded in that conversation fold again: it opens folded next time.
|
|
1322
|
+
back: async () => { await update($, agentRunsOpen, () => []); await update($, agentOpen, () => null) },
|
|
1323
|
+
earlier: () => update($, agentEarlier, n => n + 1),
|
|
1324
|
+
toggleRun: id => update($, agentRunsOpen, ids => ids.includes(id) ? ids.filter(i => i !== id) : [...ids, id]),
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* An agent's conversation for the Agents pane, read again at each draw: its steps and tool calls
|
|
1329
|
+
* redraw the pane while it runs, so what it says shows as it says it. Paths in its tool calls read
|
|
1330
|
+
* relative to the session's folder, or under `~`.
|
|
1331
|
+
*/
|
|
1332
|
+
async function agentTalk($: EngineInterface, id: string): Promise<AgentTalk> {
|
|
1333
|
+
try {
|
|
1334
|
+
const found = await $.session.messages({ agentId: id })
|
|
1335
|
+
if (!Array.isArray(found)) return { deny: found.deny }
|
|
1336
|
+
const cwd = await $.session.cwd().catch(() => undefined)
|
|
1337
|
+
return { entries: talkOf(found, { ...(cwd ? { cwd } : {}), home: await userHome($) }) }
|
|
1338
|
+
} catch (error) { return { deny: reason(error) } }
|
|
1339
|
+
}
|
|
1340
|
+
/** What an agent is doing, in the words its conversation's tool rows use (tools.ts toolDetail): no leading `cd`, paths short. */
|
|
1341
|
+
async function toolLine($: EngineInterface, e: ToolCallInput): Promise<string> {
|
|
1342
|
+
const cwd = await $.session.cwd().catch(() => undefined)
|
|
1343
|
+
return `${e.tool} ${toolDetail(e.tool, object(e), { ...(cwd ? { cwd } : {}), home: await userHome($) })}`.trim().slice(0, 70)
|
|
1161
1344
|
}
|
|
1162
1345
|
async function prepareStep($: EngineInterface, e: TurnStepInput): Promise<TurnStepInput> {
|
|
1163
1346
|
const language = await read($, lang)
|
|
@@ -1210,9 +1393,10 @@ export const register: Register = on => {
|
|
|
1210
1393
|
const result = await next(e)
|
|
1211
1394
|
jevBlips = 0
|
|
1212
1395
|
try {
|
|
1213
|
-
|
|
1396
|
+
// A failure here must not cost the commands: without them there is no /ripple to look with.
|
|
1397
|
+
try { await loadSettings($); await $.ui.status(undefined) }
|
|
1398
|
+
catch (error) { await report($, 'session.start settings', error) }
|
|
1214
1399
|
const language = await read($, lang)
|
|
1215
|
-
await $.ui.status(undefined)
|
|
1216
1400
|
await $.command.register({ name: 'ripple', description: t(language, 'commands.description'), argumentHint: t(language, 'commands.argumentHint'), immediate: true })
|
|
1217
1401
|
await $.command.register({ name: 'ripple-log', description: t(language, 'commands.logDescription'), immediate: true })
|
|
1218
1402
|
await $.command.register({ name: 'ripple-bar', description: t(language, 'commands.barDescription'), immediate: true })
|
|
@@ -1327,7 +1511,8 @@ export const register: Register = on => {
|
|
|
1327
1511
|
if (epoch !== generation) return answer
|
|
1328
1512
|
if (ownFork) { if (answer.usage) forkUsage = answer.usage; return answer }
|
|
1329
1513
|
if (e.agentId && answer.usage) {
|
|
1330
|
-
|
|
1514
|
+
const effort = sentEffort(request.model, request.effort)
|
|
1515
|
+
await update($, agents, list => list.map(a => a.id === e.agentId ? addAgentStep(a, answer.usage!, effort) : a))
|
|
1331
1516
|
} else if (!e.agentId && answer.usage) {
|
|
1332
1517
|
const now = await $.clock.now()
|
|
1333
1518
|
if (answer.usage.model === await $.session.model()) {
|
|
@@ -1349,7 +1534,13 @@ export const register: Register = on => {
|
|
|
1349
1534
|
const language = await read($, lang)
|
|
1350
1535
|
if (e.agentId) {
|
|
1351
1536
|
const now = await $.clock.now()
|
|
1537
|
+
const before = (await read($, agents)).find(a => a.id === e.agentId)
|
|
1352
1538
|
await update($, agents, list => list.map(a => a.id === e.agentId ? { ...a, state: e.reason === 'answer' ? 'done' : 'failed', endedAt: now, now: undefined, toolSince: undefined } : a))
|
|
1539
|
+
// A picked worker's outcome, once, joined to its pick by id: whether the tier it was given
|
|
1540
|
+
// (by the main chat or the judge) was enough is read from these lines later.
|
|
1541
|
+
if (before?.pick?.id && (before.state === 'running' || before.state === 'waiting')) {
|
|
1542
|
+
await persistWorkerPick($, { type: 'outcome', pick: before.pick.id, agent: e.agentId, at: new Date(now).toISOString(), reason: e.reason, aborted: e.isAborted, ms: e.durationMs, answerChars: e.answer.trim().length, model: before.model ?? null, cost: before.cost })
|
|
1543
|
+
}
|
|
1353
1544
|
} else if (!forking) {
|
|
1354
1545
|
if (await read($, busy)) await update($, busy, () => false)
|
|
1355
1546
|
const held = await read($, heldPick)
|
|
@@ -1388,18 +1579,22 @@ export const register: Register = on => {
|
|
|
1388
1579
|
const files = await workerFiles($)
|
|
1389
1580
|
try { await refreshModels($) } catch (error) { await report($, 'explicit worker catalogue', error) }
|
|
1390
1581
|
const file = files.get(e.subagentType)
|
|
1391
|
-
const provider = file ? agentProvider(file) : e.model && !/^(?:sonnet|opus|haiku|claude-)/.test(e.model) ? (await read($, routerModels)).find(m => m.value === e.model?.replace(/@[^@]*$/, ''))?.provider : 'anthropic'
|
|
1582
|
+
const provider = file ? agentProvider(file) : e.model && !/^(?:sonnet|opus|haiku|fable|mythos|claude-)/.test(e.model) ? (await read($, routerModels)).find(m => m.value === e.model?.replace(/@[^@]*$/, ''))?.provider : 'anthropic'
|
|
1392
1583
|
if (provider && s.providers[provider] === false) return { deny: t(await read($, lang), 'workers.disabled', { provider }) }
|
|
1393
1584
|
const marker = readMarker(e.prompt)
|
|
1394
|
-
|
|
1585
|
+
// A tier marker means something only to ripple:auto; on any other worker it is dropped
|
|
1586
|
+
// rather than read as a model named "hard" (which the router would refuse).
|
|
1587
|
+
if (tierOf(marker)) {
|
|
1588
|
+
input = { ...e, prompt: e.prompt.replace(MARKER, '').replace(/^\s*\n/, '') }
|
|
1589
|
+
} else if (marker) {
|
|
1395
1590
|
const markerFile = files.get(marker.name) ?? [...files.values()].find(f => agentModel(f)?.replace(/@[^@]*$/, '') === marker.name)
|
|
1396
1591
|
const markerProvider = markerFile ? agentProvider(markerFile) : (await read($, routerModels)).find(m => m.value === marker.name)?.provider
|
|
1397
1592
|
if (markerProvider && s.providers[markerProvider] === false) return { deny: t(await read($, lang), 'workers.disabled', { provider: markerProvider }) }
|
|
1398
1593
|
}
|
|
1399
|
-
const fix = await workerFix($,
|
|
1594
|
+
const fix = await workerFix($, input)
|
|
1400
1595
|
if (fix) {
|
|
1401
1596
|
if (fix.dropped) await $.ui.toast(t(await read($, lang), 'toasts.ownModel', { name: e.subagentType, model: fix.dropped }))
|
|
1402
|
-
input = { ...
|
|
1597
|
+
input = { ...input, prompt: fix.prompt, model: fix.model }
|
|
1403
1598
|
}
|
|
1404
1599
|
}
|
|
1405
1600
|
} catch (error) { input = e; await report($, 'agent.spawn prepare', error) }
|
|
@@ -1415,7 +1610,9 @@ export const register: Register = on => {
|
|
|
1415
1610
|
const result = await next(input)
|
|
1416
1611
|
try {
|
|
1417
1612
|
if (result.agentId) {
|
|
1418
|
-
const
|
|
1613
|
+
const provider = await spawnProvider($, input.subagentType, result.model)
|
|
1614
|
+
const effort = chosen?.effort ?? /@([a-z]+)$/.exec(result.model)?.[1]
|
|
1615
|
+
const rec: AgentRec = { id: result.agentId, description: input.description, type: input.subagentType, model: result.model, ...(chosen ? { pick: chosen } : {}), ...(provider ? { provider } : {}), ...(effort ? { effort } : {}), state: 'running', startedAt: await $.clock.now(), cost: 0 }
|
|
1419
1616
|
await update($, agents, list => [...list.filter(a => a.id !== rec.id), rec].slice(-40))
|
|
1420
1617
|
}
|
|
1421
1618
|
} catch (error) { await report($, 'agent.spawn record', error) }
|
|
@@ -1425,7 +1622,8 @@ export const register: Register = on => {
|
|
|
1425
1622
|
let started = 0
|
|
1426
1623
|
try {
|
|
1427
1624
|
started = await $.clock.now()
|
|
1428
|
-
|
|
1625
|
+
const line = e.agentId ? await toolLine($, e) : ''
|
|
1626
|
+
if (e.agentId) await update($, agents, list => list.map(a => a.id === e.agentId ? { ...a, now: line, toolSince: started, state: 'running', tools: (a.tools ?? 0) + 1 } : a))
|
|
1429
1627
|
} catch (error) { await report($, 'tool.call start', error) }
|
|
1430
1628
|
try { return await next(e) }
|
|
1431
1629
|
finally {
|
|
@@ -1494,7 +1692,11 @@ export const register: Register = on => {
|
|
|
1494
1692
|
return next(e)
|
|
1495
1693
|
})
|
|
1496
1694
|
on('ui.close', { id: AGENTS_PANE }, async ($, e, next) => {
|
|
1497
|
-
try {
|
|
1695
|
+
try {
|
|
1696
|
+
await update($, agentsOpen, () => false)
|
|
1697
|
+
// Opened again, the pane starts on the list.
|
|
1698
|
+
if (await read($, agentOpen) !== null) await update($, agentOpen, () => null)
|
|
1699
|
+
} catch (error) { await report($, 'agents close', error) }
|
|
1498
1700
|
return next(e)
|
|
1499
1701
|
})
|
|
1500
1702
|
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
|
|
@@ -1554,7 +1756,11 @@ export const register: Register = on => {
|
|
|
1554
1756
|
try {
|
|
1555
1757
|
startTimers($)
|
|
1556
1758
|
const frame = await frameOf($, e)
|
|
1557
|
-
|
|
1759
|
+
// Read to be drawn again each second an agent runs (agentsClock): its elapsed time ticks.
|
|
1760
|
+
await read($, agentsTick)
|
|
1761
|
+
const id = await read($, agentOpen)
|
|
1762
|
+
const open = id === null ? undefined : { id, talk: await agentTalk($, id), earlier: await read($, agentEarlier), runsOpen: await read($, agentRunsOpen) }
|
|
1763
|
+
return renderAgents($.ui.resolve(e), frame, { agents: await read($, agents), now: frame.now, showDone: await read($, agentsShowDone), ...(open ? { open } : {}) }, agentsActions($)) as RenderElement
|
|
1558
1764
|
} catch (error) { await report($, 'agents render', error); return next(e) }
|
|
1559
1765
|
})
|
|
1560
1766
|
}
|