@aragon-agent/cli 0.6.23 → 0.6.25
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 +1835 -1786
- package/README.md +2578 -2544
- package/dist/agent/controller.d.ts +60 -3
- package/dist/agent/controller.js +111 -6
- package/dist/agent/conversation-reset.d.ts +91 -0
- package/dist/agent/conversation-reset.js +66 -0
- package/dist/agent/model-probe-limits.d.ts +39 -0
- package/dist/agent/model-probe-limits.js +25 -0
- package/dist/agent/model-probe.d.ts +104 -0
- package/dist/agent/model-probe.js +391 -0
- package/dist/agent/model-profile-settings.js +29 -2
- package/dist/agent/reducer.js +45 -13
- package/dist/commands/builtins.js +101 -37
- package/dist/commands/clear-command.d.ts +27 -0
- package/dist/commands/clear-command.js +107 -0
- package/dist/commands/registry.d.ts +20 -1
- package/dist/compaction/child.d.ts +15 -0
- package/dist/compaction/child.js +14 -3
- package/dist/compaction/compactor.js +1 -0
- package/dist/compaction/context-command.js +10 -1
- package/dist/compaction/meter.d.ts +9 -0
- package/dist/compaction/meter.js +16 -1
- package/dist/compaction/pressure.d.ts +7 -0
- package/dist/compaction/pressure.js +5 -0
- package/dist/compaction/types.d.ts +11 -0
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/cli-commands.d.ts +1 -1
- package/dist/config/cli-commands.js +12 -0
- package/dist/config/load.js +24 -6
- package/dist/config/model-profile-resolution.d.ts +8 -0
- package/dist/config/model-profile-resolution.js +13 -6
- package/dist/config/model-profile-store.d.ts +4 -2
- package/dist/config/model-profiles.d.ts +21 -0
- package/dist/config/model-profiles.js +28 -2
- package/dist/config/schema.d.ts +61 -0
- package/dist/config/schema.js +36 -0
- package/dist/session/clear-archive.d.ts +51 -0
- package/dist/session/clear-archive.js +160 -0
- package/dist/team/model.d.ts +88 -0
- package/dist/team/model.js +136 -0
- package/dist/team/report.d.ts +11 -0
- package/dist/team/report.js +24 -1
- package/dist/team/subagent.js +37 -21
- package/dist/team/task-tool.d.ts +25 -6
- package/dist/team/task-tool.js +15 -9
- package/dist/todo/types.d.ts +5 -3
- package/dist/ui/App.js +44 -7
- package/dist/ui/entries/CompactionCard.d.ts +5 -4
- package/dist/ui/entries/CompactionCard.js +5 -4
- package/dist/ui/overlays/HelpOverlay.js +8 -2
- package/dist/ui/overlays/ModelDiscoveryList.d.ts +23 -0
- package/dist/ui/overlays/ModelDiscoveryList.js +161 -0
- package/dist/ui/overlays/ModelProfileEditor.d.ts +18 -1
- package/dist/ui/overlays/ModelProfileEditor.js +117 -54
- package/dist/ui/overlays/ModelProfilePicker.d.ts +14 -2
- package/dist/ui/overlays/ModelProfilePicker.js +51 -10
- package/dist/ui/overlays/QuestionOverlay.js +47 -6
- package/dist/ui/overlays/SettingsScreen.d.ts +17 -0
- package/dist/ui/overlays/SettingsScreen.js +152 -18
- package/dist/ui/overlays/model-probe-view.d.ts +128 -0
- package/dist/ui/overlays/model-probe-view.js +225 -0
- package/dist/ui/overlays/model-profile-state.d.ts +62 -5
- package/dist/ui/overlays/model-profile-state.js +121 -14
- package/dist/ui/overlays/use-model-probe.d.ts +30 -0
- package/dist/ui/overlays/use-model-probe.js +109 -0
- package/package.json +2 -2
|
@@ -16,9 +16,12 @@ import { buildContinuationMessage } from '../todo/follow-through.js';
|
|
|
16
16
|
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
17
17
|
import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
|
|
18
18
|
import { getLogger } from '../logging/logger.js';
|
|
19
|
+
import { resolveFastTier } from '../fast/resolve.js';
|
|
20
|
+
import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
|
|
19
21
|
import { registerQueueCommand } from './queue-command.js';
|
|
20
22
|
import { perfCommand } from './perf.js';
|
|
21
23
|
import { runCompactCommand } from '../compaction/command.js';
|
|
24
|
+
import { runClearCommand } from './clear-command.js';
|
|
22
25
|
import { runContextCommand } from '../compaction/context-command.js';
|
|
23
26
|
/**
|
|
24
27
|
* `/fast status` — THE GUARANTEED REPORTING SURFACE for the tier (§4.4 / R-8).
|
|
@@ -90,6 +93,66 @@ function formatUpdateStatus(port, override) {
|
|
|
90
93
|
lines.push(` next ${next ? new Date(next).toLocaleTimeString() : 'not scheduled'}`);
|
|
91
94
|
return lines.join('\n');
|
|
92
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* `/team model [main|fast|<profile-name>]` (subagent-model §3.6 / 5.2).
|
|
98
|
+
*
|
|
99
|
+
* The user-owned switch's command-line surface. `main` and `fast` are verbs
|
|
100
|
+
* compared case-insensitively; a NAME is matched EXACTLY, case and all,
|
|
101
|
+
* against the library - the same identity the picker shows - and a miss lists
|
|
102
|
+
* the names that would work rather than failing silently. The summary reuses
|
|
103
|
+
* the exact wording the settings root row renders, so the two surfaces can
|
|
104
|
+
* never describe the same setting differently.
|
|
105
|
+
*/
|
|
106
|
+
function runTeamModelCommand(ctx, subject) {
|
|
107
|
+
const controller = ctx.controller;
|
|
108
|
+
const config = controller.getConfig();
|
|
109
|
+
const tier = resolveFastTier(config, (id, role) => controller.hasApiKey(id, role));
|
|
110
|
+
const summary = subagentModelSummary({
|
|
111
|
+
mode: config.team.model,
|
|
112
|
+
modelProfileId: config.team.modelProfileId,
|
|
113
|
+
mainConnection: `${config.provider}:${config.model}`,
|
|
114
|
+
fastConnection: tier.ok ? `${tier.ref.providerId}:${tier.ref.modelId}` : null,
|
|
115
|
+
profiles: config.modelProfiles,
|
|
116
|
+
hasKey: (id) => controller.hasApiKey(id),
|
|
117
|
+
});
|
|
118
|
+
if (!subject) {
|
|
119
|
+
// The status readout: mode + the connection actually in effect + the
|
|
120
|
+
// disclosed reason when the request had to fall back (D-4).
|
|
121
|
+
const resolution = controller.subagentModelResolution();
|
|
122
|
+
const note = resolution.fallbackReason
|
|
123
|
+
? ` Unavailable (${resolution.fallbackReason}); children run on the main model.`
|
|
124
|
+
: resolution.effective === 'main'
|
|
125
|
+
? ''
|
|
126
|
+
: ` Children run on ${resolution.ref.providerId}:${resolution.ref.modelId}.`;
|
|
127
|
+
ctx.notify('info', `Subagent model: ${summary}.${note}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (subject.toLowerCase() === 'main' || subject.toLowerCase() === 'fast') {
|
|
131
|
+
const mode = subject.toLowerCase();
|
|
132
|
+
const applied = controller.setTeamConfig({ model: mode });
|
|
133
|
+
ctx.persistConfig({ team: { model: applied.model } });
|
|
134
|
+
if (mode === 'fast' && !tier.ok) {
|
|
135
|
+
ctx.notify('warn', 'The fast tier is not available now; subagents will fall back to the main model.');
|
|
136
|
+
}
|
|
137
|
+
ctx.toast('success', `Subagent model: ${mode === 'main' ? 'same as main' : 'fast tier'}.`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const entries = config.modelProfiles?.entries ?? [];
|
|
141
|
+
const matches = entries.filter((entry) => entry.name === subject);
|
|
142
|
+
if (matches.length === 0) {
|
|
143
|
+
const names = entries.slice(0, 8).map((entry) => entry.name);
|
|
144
|
+
ctx.notify('warn', `No profile named "${subject}".`
|
|
145
|
+
+ (names.length ? ` Available: ${names.join(', ')}` : ' Create one in /settings first.'));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const entry = matches[0];
|
|
149
|
+
const disambiguation = matches.length > 1 ? ` (#${entry.id.slice(0, 8)})` : '';
|
|
150
|
+
const applied = controller.setTeamConfig({ model: 'profile', modelProfileId: entry.id });
|
|
151
|
+
const teamPatch = { model: applied.model,
|
|
152
|
+
modelProfileId: applied.modelProfileId };
|
|
153
|
+
ctx.persistConfig({ team: teamPatch });
|
|
154
|
+
ctx.toast('success', `Subagent model: ${entry.name}${disambiguation} (${entry.provider}:${entry.model}).`);
|
|
155
|
+
}
|
|
93
156
|
function formatFastStatus(controller) {
|
|
94
157
|
const status = controller.getFastStatus();
|
|
95
158
|
const cfg = controller.getFastConfig();
|
|
@@ -254,7 +317,7 @@ const COMMANDS = [
|
|
|
254
317
|
},
|
|
255
318
|
{
|
|
256
319
|
name: 'team',
|
|
257
|
-
description: 'Team subagents: status | on | off | max <n> | overseer on|off',
|
|
320
|
+
description: 'Team subagents: status | on | off | max <n> | overseer on|off | model [main|fast|<profile-name>]',
|
|
258
321
|
/**
|
|
259
322
|
* `/team on` CANNOT turn team mode on in a session that started without it,
|
|
260
323
|
* and this command must not pretend otherwise (§4.5 / D-17 / P0-2).
|
|
@@ -275,6 +338,12 @@ const COMMANDS = [
|
|
|
275
338
|
const arg = ctx.args.trim().toLowerCase();
|
|
276
339
|
const [verb, value] = arg.split(/\s+/, 2);
|
|
277
340
|
const controller = ctx.controller;
|
|
341
|
+
// RAW TOKENS, KEPT BESIDE THE LOWERCASED PAIR (V-2): `model` matches a
|
|
342
|
+
// profile NAME case-sensitively - like the picker and the library -
|
|
343
|
+
// and the whole-args toLowerCase() above would have made
|
|
344
|
+
// `/team model MyProfile` silently unmatchable. Only the VERB token is
|
|
345
|
+
// compared through the lowercased pair.
|
|
346
|
+
const rawTokens = ctx.args.trim().split(/\s+/).filter((token) => token.length > 0);
|
|
278
347
|
// Refused mid-dispatch for the reason `/reload` already records: half a
|
|
279
348
|
// dispatch under one ceiling and half under another produces a result
|
|
280
349
|
// nothing can afterwards explain.
|
|
@@ -284,6 +353,7 @@ const COMMANDS = [
|
|
|
284
353
|
}
|
|
285
354
|
if (!verb || verb === 'status') {
|
|
286
355
|
const cfg = controller.getTeamConfig();
|
|
356
|
+
const config = controller.getConfig();
|
|
287
357
|
const state = !controller.isTeamRegistered()
|
|
288
358
|
? 'off for this session (started with --no-team)'
|
|
289
359
|
: controller.isTeamEnabled()
|
|
@@ -293,9 +363,20 @@ const COMMANDS = [
|
|
|
293
363
|
const roster = snapshot
|
|
294
364
|
? `\nRunning: ${snapshot.runs.map((r) => `${r.label} (${r.phase})`).join(', ')}`
|
|
295
365
|
: '';
|
|
366
|
+
const tier = resolveFastTier(controller.getConfig(), (id, role) => controller.hasApiKey(id, role));
|
|
367
|
+
const subagent = subagentModelSummary({
|
|
368
|
+
mode: cfg.model,
|
|
369
|
+
modelProfileId: cfg.modelProfileId,
|
|
370
|
+
mainConnection: `${config.provider}:${config.model}`,
|
|
371
|
+
fastConnection: tier.ok ? `${tier.ref.providerId}:${tier.ref.modelId}` : null,
|
|
372
|
+
profiles: config.modelProfiles,
|
|
373
|
+
hasKey: (id) => controller.hasApiKey(id),
|
|
374
|
+
});
|
|
296
375
|
ctx.notify('info', `Team mode: ${state}. Max subagents: ${cfg.maxSubagents}, ` +
|
|
297
376
|
`concurrent: ${cfg.maxConcurrent}, supervisor: ${cfg.overseer ? 'on' : 'off'}` +
|
|
298
|
-
`${cfg.overseer ? ' (never kills; checks the fast tier each tick)' : ' (legacy hard timeouts)'}
|
|
377
|
+
`${cfg.overseer ? ' (never kills; checks the fast tier each tick)' : ' (legacy hard timeouts)'}.` +
|
|
378
|
+
`\nSubagent model: ${subagent}` +
|
|
379
|
+
`${roster}`);
|
|
299
380
|
return;
|
|
300
381
|
}
|
|
301
382
|
if (verb === 'on' || verb === 'off') {
|
|
@@ -332,6 +413,10 @@ const COMMANDS = [
|
|
|
332
413
|
ctx.toast('success', `Max subagents set to ${applied.maxSubagents}.`);
|
|
333
414
|
return;
|
|
334
415
|
}
|
|
416
|
+
if (verb === 'model') {
|
|
417
|
+
runTeamModelCommand(ctx, rawTokens[1]);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
335
420
|
if (verb === 'overseer') {
|
|
336
421
|
if (value !== 'on' && value !== 'off') {
|
|
337
422
|
ctx.notify('warn', 'Usage: /team overseer on|off');
|
|
@@ -347,7 +432,7 @@ const COMMANDS = [
|
|
|
347
432
|
ctx.toast('success', `Dispatch supervisor ${applied.overseer ? 'on' : 'off'}.`);
|
|
348
433
|
return;
|
|
349
434
|
}
|
|
350
|
-
ctx.notify('warn', `Unknown argument "${verb}" - use /team [status|on|off|max <n>|overseer on|off].`);
|
|
435
|
+
ctx.notify('warn', `Unknown argument "${verb}" - use /team [status|on|off|max <n>|overseer on|off|model main|fast|<profile-name>].`);
|
|
351
436
|
},
|
|
352
437
|
},
|
|
353
438
|
{
|
|
@@ -873,44 +958,23 @@ const COMMANDS = [
|
|
|
873
958
|
},
|
|
874
959
|
},
|
|
875
960
|
{
|
|
961
|
+
// REGISTRATION ONLY. The body lives in `commands/clear-command.ts`: this file
|
|
962
|
+
// is already past the 1000-line guideline, and `/clear` is no longer a
|
|
963
|
+
// two-line projection reset - it interrupts a run, waits for it, archives the
|
|
964
|
+
// conversation and then clears (clear-command-context-reset D-8).
|
|
876
965
|
name: 'clear',
|
|
877
|
-
description: '
|
|
878
|
-
run:
|
|
879
|
-
// A USER OVERRIDE OF A PROJECTION, the same class as `/todo clear`, so it
|
|
880
|
-
// reuses reason `'user'`; I-2 and its standing exception are stated on
|
|
881
|
-
// `TodoStore`. Clearing the STORE and not merely the view mirror is what
|
|
882
|
-
// keeps `/save`, `/todo status|continue` and follow-through agreeing with
|
|
883
|
-
// the screen — they read the store directly. `messages` is untouched:
|
|
884
|
-
// clearing the conversation is still `/reset` alone.
|
|
885
|
-
//
|
|
886
|
-
// DELIBERATELY WITHOUT `/todo clear`'s two guards: an `isRunning()` refusal
|
|
887
|
-
// would leave `/clear` unable to clear even the transcript mid-run, and an
|
|
888
|
-
// early return on an empty list would skip the dispatch below entirely.
|
|
889
|
-
// Clearing an empty store is already silent — it emits into a reducer
|
|
890
|
-
// branch that writes two fields that are both already at their defaults.
|
|
891
|
-
//
|
|
892
|
-
// Bounded side effect: the emptied store re-arms `todo_write`'s "a fresh
|
|
893
|
-
// plan has at least two items" gate, so a model that shrinks a plan to one
|
|
894
|
-
// item right after a mid-run `/clear` gets the refusal text and retries.
|
|
895
|
-
ctx.controller.clearTodos();
|
|
896
|
-
ctx.dispatch({ type: 'clearTranscript' });
|
|
897
|
-
},
|
|
966
|
+
description: 'Start a new conversation; "/clear screen" only clears the screen',
|
|
967
|
+
run: runClearCommand,
|
|
898
968
|
},
|
|
899
969
|
{
|
|
970
|
+
// ALIAS BY DESIGN (D-2), AND THIS IS THE WHOLE FIX. Until this change `/clear`
|
|
971
|
+
// cleared the screen while `/reset` cleared the session, so the command a
|
|
972
|
+
// user reaches for in a long conversation was the one that kept the history.
|
|
973
|
+
// One implementation makes that drift structurally impossible rather than
|
|
974
|
+
// merely fixed: there is no second body left to diverge.
|
|
900
975
|
name: 'reset',
|
|
901
|
-
description: '
|
|
902
|
-
run:
|
|
903
|
-
if (ctx.controller.isRunning()) {
|
|
904
|
-
ctx.notify('warn', 'Cannot reset while the agent is running. Wait for it to stop.');
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
ctx.controller.clearMessages();
|
|
908
|
-
ctx.controller.clearAllQueues();
|
|
909
|
-
ctx.dispatch({ type: 'resetConversation' });
|
|
910
|
-
ctx.toast('info', 'Started a new conversation.');
|
|
911
|
-
if (ctx.state.pendingSteering.length)
|
|
912
|
-
ctx.notify('warn', `\u5df2\u53d6\u6d88 ${ctx.state.pendingSteering.length} \u6761\u672a\u63a5\u6536\u6d88\u606f`);
|
|
913
|
-
},
|
|
976
|
+
description: 'Alias of /clear',
|
|
977
|
+
run: runClearCommand,
|
|
914
978
|
},
|
|
915
979
|
{
|
|
916
980
|
name: 'cwd',
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/clear` - start a new conversation - plus its `/reset` alias and the
|
|
3
|
+
* `/clear screen` form that keeps the old "just wipe the screen" meaning
|
|
4
|
+
* (clear-command-context-reset §3.5 / D-1 / D-2 / D-3).
|
|
5
|
+
*
|
|
6
|
+
* ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`. The one
|
|
7
|
+
* localized string below is written with `\u` escapes for that reason, exactly
|
|
8
|
+
* as the `/reset` body it replaces did.
|
|
9
|
+
*
|
|
10
|
+
* WHY THE BODY LIVES HERE AND THE REGISTRATION STAYS IN `builtins.ts`: the same
|
|
11
|
+
* reason `/compact` and `/context` do (D-8). `builtins.ts` is 1511 lines against
|
|
12
|
+
* `CLAUDE.md`'s 1000-line cap, and this command is no longer a two-line
|
|
13
|
+
* projection reset - it interrupts a run, waits for it, archives a conversation
|
|
14
|
+
* and then clears. A two-line edit belongs inline; this does not.
|
|
15
|
+
*
|
|
16
|
+
* THE COMMAND'S ONLY JOB IS SEQUENCING AND SPEAKING. Every state change happens
|
|
17
|
+
* on the controller port (`AgentController.resetConversation`, which delegates
|
|
18
|
+
* the ORDER to `agent/conversation-reset.ts`), and every word the user reads is
|
|
19
|
+
* decided here, in one place, in the exact order they will read it.
|
|
20
|
+
*/
|
|
21
|
+
import type { CommandContext } from './registry.js';
|
|
22
|
+
/**
|
|
23
|
+
* Run `/clear` or `/reset`. Both registry entries point HERE (D-2), which is
|
|
24
|
+
* what stops the two from drifting back into "one clears the screen, one clears
|
|
25
|
+
* the session" - the drift that produced the defect this feature fixes.
|
|
26
|
+
*/
|
|
27
|
+
export declare function runClearCommand(ctx: CommandContext): Promise<void>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/clear` - start a new conversation - plus its `/reset` alias and the
|
|
3
|
+
* `/clear screen` form that keeps the old "just wipe the screen" meaning
|
|
4
|
+
* (clear-command-context-reset §3.5 / D-1 / D-2 / D-3).
|
|
5
|
+
*
|
|
6
|
+
* ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`. The one
|
|
7
|
+
* localized string below is written with `\u` escapes for that reason, exactly
|
|
8
|
+
* as the `/reset` body it replaces did.
|
|
9
|
+
*
|
|
10
|
+
* WHY THE BODY LIVES HERE AND THE REGISTRATION STAYS IN `builtins.ts`: the same
|
|
11
|
+
* reason `/compact` and `/context` do (D-8). `builtins.ts` is 1511 lines against
|
|
12
|
+
* `CLAUDE.md`'s 1000-line cap, and this command is no longer a two-line
|
|
13
|
+
* projection reset - it interrupts a run, waits for it, archives a conversation
|
|
14
|
+
* and then clears. A two-line edit belongs inline; this does not.
|
|
15
|
+
*
|
|
16
|
+
* THE COMMAND'S ONLY JOB IS SEQUENCING AND SPEAKING. Every state change happens
|
|
17
|
+
* on the controller port (`AgentController.resetConversation`, which delegates
|
|
18
|
+
* the ORDER to `agent/conversation-reset.ts`), and every word the user reads is
|
|
19
|
+
* decided here, in one place, in the exact order they will read it.
|
|
20
|
+
*/
|
|
21
|
+
import { getLogger } from '../logging/logger.js';
|
|
22
|
+
/** The toast a successful clear shows. Pinned verbatim by an existing test. */
|
|
23
|
+
const CLEARED_TOAST = 'Started a new conversation.';
|
|
24
|
+
/**
|
|
25
|
+
* Run `/clear` or `/reset`. Both registry entries point HERE (D-2), which is
|
|
26
|
+
* what stops the two from drifting back into "one clears the screen, one clears
|
|
27
|
+
* the session" - the drift that produced the defect this feature fixes.
|
|
28
|
+
*/
|
|
29
|
+
export async function runClearCommand(ctx) {
|
|
30
|
+
const verb = ctx.args.trim().toLowerCase();
|
|
31
|
+
if (verb === 'screen') {
|
|
32
|
+
// The old behaviour, byte for byte (I-3): silent, transcript plus the todo
|
|
33
|
+
// panel, history untouched.
|
|
34
|
+
clearScreenOnly(ctx);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (verb !== '') {
|
|
38
|
+
// Rejected rather than ignored. A command that silently accepts and
|
|
39
|
+
// discards an argument is a surface that lies about what it understood.
|
|
40
|
+
ctx.notify('warn', `Unknown argument "${ctx.args.trim()}" - use /clear [screen].`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// READ BEFORE CLEARING (P1-2 / I-9). `clearAllQueues()` empties
|
|
44
|
+
// `pendingUserSteering`, so a count taken afterwards is always zero - and a
|
|
45
|
+
// notice that says "0 messages cancelled" is worse than no notice at all.
|
|
46
|
+
const cancelled = ctx.state.pendingSteering.length;
|
|
47
|
+
const result = await ctx.controller.resetConversation({
|
|
48
|
+
interrupt: true,
|
|
49
|
+
// A THUNK, NOT A VALUE (P0-1 / §3.3). `ctx.state` is the view as of command
|
|
50
|
+
// entry, and the archive happens AFTER the interrupted engine unwinds - so
|
|
51
|
+
// the last tool cards of the abandoned run are written to the transcript in
|
|
52
|
+
// between. A value captured here would archive everything except the part
|
|
53
|
+
// the user was looking at when they typed the command.
|
|
54
|
+
readTranscript: () => ctx.transcriptEntries?.() ?? ctx.state.entries,
|
|
55
|
+
});
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
// ZERO SIDE EFFECTS (I-4), and a trace (P1-5). The user gets one sentence
|
|
58
|
+
// naming the next action; the log gets the reason, because "my /clear did
|
|
59
|
+
// nothing" is otherwise unanswerable after the fact.
|
|
60
|
+
getLogger().warn('agent', 'clear_still_running', { reason: result.reason });
|
|
61
|
+
ctx.notify('warn', result.message);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// THE VIEW GOES LAST (B-5 / §3.3). `App`'s dispatch wrapper zeroes the run
|
|
65
|
+
// metrics, the interaction phase and the scroll anchor on this action, so
|
|
66
|
+
// dispatching it after the engine has settled is what makes "cleared" mean
|
|
67
|
+
// "nothing is still writing into it".
|
|
68
|
+
ctx.dispatch({ type: 'resetConversation' });
|
|
69
|
+
// THE ONE CONSUMER OF `messagesCleared` (P2-1). It cannot join the toast:
|
|
70
|
+
// `clear-command-todo.test.ts` pins that string verbatim, and a count nobody
|
|
71
|
+
// asked for is noise in the one place the user reads confirmations.
|
|
72
|
+
getLogger().info('agent', 'conversation_cleared', {
|
|
73
|
+
messagesCleared: result.messagesCleared,
|
|
74
|
+
interrupted: result.interrupted,
|
|
75
|
+
});
|
|
76
|
+
ctx.toast('info', CLEARED_TOAST);
|
|
77
|
+
// REINSTATED FROM THE OLD `/reset` (P1-1 / C-3). Dropping it would silently
|
|
78
|
+
// delete a user-visible acknowledgement that already existed and was already
|
|
79
|
+
// pinned by test.
|
|
80
|
+
if (cancelled) {
|
|
81
|
+
ctx.notify('warn', `\u5df2\u53d6\u6d88 ${cancelled} \u6761\u672a\u63a5\u6536\u6d88\u606f`);
|
|
82
|
+
}
|
|
83
|
+
if (result.archivedPath) {
|
|
84
|
+
// A PATH RATHER THAN A PROMISE: the user can copy it, and `/resume` accepts
|
|
85
|
+
// an absolute path. "Archived" with no way to find the file is a reassurance,
|
|
86
|
+
// not a recovery.
|
|
87
|
+
ctx.notify('info', `Previous conversation archived: ${result.archivedPath} (/resume <path> to restore)`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Today's `/clear`, unchanged and silent (D-3 / I-3).
|
|
92
|
+
*
|
|
93
|
+
* Kept as a real form rather than as a separate command because "clear the
|
|
94
|
+
* screen" and "clear the conversation" are two different intentions, and a user
|
|
95
|
+
* who wants the first must not have to risk the second. It also gives
|
|
96
|
+
* `clearTranscript` a live dispatcher: `CLAUDE.md` forbids orphaned actions, and
|
|
97
|
+
* this was the last one that would have had none.
|
|
98
|
+
*/
|
|
99
|
+
function clearScreenOnly(ctx) {
|
|
100
|
+
// A USER OVERRIDE OF A PROJECTION, the same class as `/todo clear`, so it
|
|
101
|
+
// reuses reason `'user'`; the distinction against `clearMessages()`'s `'reset'`
|
|
102
|
+
// is recorded on `TodoStore`. Clearing the STORE and not merely the view
|
|
103
|
+
// mirror is what keeps `/save`, `/todo status|continue` and follow-through
|
|
104
|
+
// agreeing with the screen - they read the store directly.
|
|
105
|
+
ctx.controller.clearTodos();
|
|
106
|
+
ctx.dispatch({ type: 'clearTranscript' });
|
|
107
|
+
}
|
|
@@ -6,7 +6,7 @@ import type { SubmitMessageOptions } from '../agent/prompt-options.js';
|
|
|
6
6
|
import type { CopyRequest, CopyRequestResult } from '../ui/clipboard-task.js';
|
|
7
7
|
import type { AgentController } from '../agent/controller.js';
|
|
8
8
|
import type { AgentMode } from '../agent/agent-mode.js';
|
|
9
|
-
import type { NoticeLevel, Overlay, ToastLevel, ViewAction, ViewState } from '../agent/reducer.js';
|
|
9
|
+
import type { Entry, NoticeLevel, Overlay, ToastLevel, ViewAction, ViewState } from '../agent/reducer.js';
|
|
10
10
|
import type { PersistedConfig } from '../config/schema.js';
|
|
11
11
|
import type { FollowThroughBudget } from '../todo/follow-through.js';
|
|
12
12
|
import type { UpdateCommandPort } from '../update/types.js';
|
|
@@ -108,6 +108,25 @@ export interface CommandContext {
|
|
|
108
108
|
* stream is handed over directly.
|
|
109
109
|
*/
|
|
110
110
|
writeForeign?: (text: string) => void;
|
|
111
|
+
/**
|
|
112
|
+
* The LIVE transcript rows, for `/clear`'s archive (clear-command-context-reset
|
|
113
|
+
* P0-1 / §3.5).
|
|
114
|
+
*
|
|
115
|
+
* A THUNK, AND THAT IS THE WHOLE POINT. The `state` field above is a snapshot
|
|
116
|
+
* of `ViewState` taken when the command started; `/clear` interrupts a run and
|
|
117
|
+
* then waits for the engine to unwind, so the last tool cards of that run are
|
|
118
|
+
* written to the transcript AFTER `state` was captured. Reading `state.entries`
|
|
119
|
+
* would archive everything except the part the user was just looking at.
|
|
120
|
+
*
|
|
121
|
+
* OPTIONAL, like `followBudget` / `update` / `mouse`: every existing
|
|
122
|
+
* `CommandContext` fixture in this package builds the object by literal, and a
|
|
123
|
+
* required field would break all of them at compile time for a feature none of
|
|
124
|
+
* them are about. Absent means the archive degrades to history-only.
|
|
125
|
+
*
|
|
126
|
+
* `Entry` arrives as a TYPE-ONLY import so this module keeps its runtime shape
|
|
127
|
+
* (the same rule the `update/types.js` import above follows).
|
|
128
|
+
*/
|
|
129
|
+
transcriptEntries?: () => Entry[];
|
|
111
130
|
}
|
|
112
131
|
/** What `/mouse` can see and do. Read at command time, never captured. */
|
|
113
132
|
export interface MouseCommandPort {
|
|
@@ -56,6 +56,21 @@ export interface ChildCompactionDeps {
|
|
|
56
56
|
export interface ChildContextManagerRequest {
|
|
57
57
|
role?: ModelRole;
|
|
58
58
|
label: string;
|
|
59
|
+
/**
|
|
60
|
+
* REQ-LEVEL key resolvers, preferred over the lead's (V-4).
|
|
61
|
+
*
|
|
62
|
+
* `wiring.ts::childFactory` injects the LEAD layer's `getApiKey`/`hasKey`,
|
|
63
|
+
* which cannot see a fast-only key (`role='fast'` only) or a profile's
|
|
64
|
+
* dedicated key (it exists only inside the entry). `Compactor.resolveSummarizer`
|
|
65
|
+
* gates its main choice on `hasKey(provider, role)` and reads the call key
|
|
66
|
+
* through `getApiKey`, so without these a `team.model: fast/profile` child
|
|
67
|
+
* whose only key lives in one of those places would have its compaction
|
|
68
|
+
* SILENTLY DISABLED - the summarizer resolves to null and nobody says why.
|
|
69
|
+
* `subagent.ts` passes the same profile-aware closures the child's own
|
|
70
|
+
* `Agent` gets; ABSENT keeps today's behavior byte for byte (matrix 24).
|
|
71
|
+
*/
|
|
72
|
+
getApiKey?: (providerId: string, role?: ModelRole) => string | undefined;
|
|
73
|
+
hasKey?: (providerId: string, role?: ModelRole) => boolean;
|
|
59
74
|
/**
|
|
60
75
|
* The model the child actually runs on.
|
|
61
76
|
*
|
package/dist/compaction/child.js
CHANGED
|
@@ -65,6 +65,10 @@ export function createChildContextManager(req, deps) {
|
|
|
65
65
|
baseUrl: req.model.baseUrl,
|
|
66
66
|
// The lead's manual limit describes its connection, not every worker.
|
|
67
67
|
contextWindow: sameConnection ? base.contextWindow : null,
|
|
68
|
+
// THE ORIGIN FOLLOWS THE VALUE through the same gate, honoring the iff
|
|
69
|
+
// contract (value null => origin undefined): a child on a different
|
|
70
|
+
// connection inherits neither the number nor its provenance.
|
|
71
|
+
contextWindowOrigin: sameConnection ? base.contextWindowOrigin : undefined,
|
|
68
72
|
// NO POLICY OVERLAY (main-agent parity): `keepRecentTurns`, `onFailure`,
|
|
69
73
|
// `useFastTier` and `archive` are read LIVE from the base config, so the
|
|
70
74
|
// child compacts under exactly the policy the lead compacts under. A
|
|
@@ -81,15 +85,22 @@ export function createChildContextManager(req, deps) {
|
|
|
81
85
|
return source === undefined ? deps.isPricedModel(req.model) : source !== 'fallback';
|
|
82
86
|
},
|
|
83
87
|
getWindowOverride: () => childConfig().contextWindow,
|
|
88
|
+
getWindowOverrideOrigin: () => childConfig().contextWindowOrigin,
|
|
84
89
|
});
|
|
85
90
|
let overflowNotified = false;
|
|
91
|
+
// REQ-LEVEL RESOLVERS WIN (V-4), and the fallback is the lead layer - so a
|
|
92
|
+
// request built without them behaves exactly as before, while one built by
|
|
93
|
+
// `subagent.ts` under `team.model: fast/profile` lets the summarizer see
|
|
94
|
+
// the same keys the child itself authenticates with.
|
|
95
|
+
const hasKey = req.hasKey ?? deps.hasKey;
|
|
96
|
+
const getApiKey = req.getApiKey ?? deps.getApiKey;
|
|
86
97
|
const compactor = new Compactor({
|
|
87
98
|
getConfig: childConfig,
|
|
88
99
|
meter,
|
|
89
100
|
mainRole: req.role ?? 'main',
|
|
90
|
-
resolveFastCandidate: () => resolveFastTier(deps.getConfig(),
|
|
91
|
-
hasKey
|
|
92
|
-
getApiKey
|
|
101
|
+
resolveFastCandidate: () => resolveFastTier(deps.getConfig(), hasKey),
|
|
102
|
+
hasKey,
|
|
103
|
+
getApiKey,
|
|
93
104
|
getModelInfoFor: deps.getModelInfoFor,
|
|
94
105
|
isPricedModel: deps.isPricedModel,
|
|
95
106
|
getMessages: req.getMessages,
|
|
@@ -403,6 +403,7 @@ export class Compactor {
|
|
|
403
403
|
return source === undefined ? this.deps.isPricedModel(this.mainRef()) : source !== 'fallback';
|
|
404
404
|
},
|
|
405
405
|
getWindowOverride: () => this.deps.getConfig().contextWindow,
|
|
406
|
+
getWindowOverrideOrigin: () => this.deps.getConfig().contextWindowOrigin,
|
|
406
407
|
});
|
|
407
408
|
return this.ownMeter;
|
|
408
409
|
}
|
|
@@ -87,7 +87,16 @@ function occupancyLine(usage) {
|
|
|
87
87
|
*/
|
|
88
88
|
function windowLine(usage) {
|
|
89
89
|
if (usage.windowOverridden) {
|
|
90
|
-
|
|
90
|
+
// THREE NAMED SURFACES (profile-context-window §6.2). `undefined` falls
|
|
91
|
+
// into the config branch ON PURPOSE: that string is what every pre-feature
|
|
92
|
+
// test asserts, and a meter built without the origin dep must report
|
|
93
|
+
// byte-identically (invariant 6).
|
|
94
|
+
const named = usage.windowOverrideOrigin === 'profile'
|
|
95
|
+
? 'from your model profile'
|
|
96
|
+
: usage.windowOverrideOrigin === 'env'
|
|
97
|
+
? 'from ARAGON_CONTEXT_WINDOW'
|
|
98
|
+
: 'from contextWindow configuration';
|
|
99
|
+
return `${usage.window} ${named} (overrides API, model table, and model-windows.json)`;
|
|
91
100
|
}
|
|
92
101
|
if (!usage.windowKnown) {
|
|
93
102
|
return (`UNKNOWN - no verified model limit; internal fallback budget ${usage.window}; ` +
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* `current()` carries a structural belt for a missed invalidation site.
|
|
29
29
|
*/
|
|
30
30
|
import type { AgentEvent, Message, ModelInfo, TokenUsage } from '@aragon-agent/core';
|
|
31
|
+
import type { ContextWindowOrigin } from '../config/schema.js';
|
|
31
32
|
import type { ContextUsageSnapshot, Pressure } from './types.js';
|
|
32
33
|
/**
|
|
33
34
|
* How long a scheduled publish waits.
|
|
@@ -62,6 +63,14 @@ export interface ContextMeterDeps {
|
|
|
62
63
|
* measurement, so a live settings edit moves the denominator without a relaunch.
|
|
63
64
|
*/
|
|
64
65
|
getWindowOverride(): number | null;
|
|
66
|
+
/**
|
|
67
|
+
* WHERE the override above came from, read live beside it
|
|
68
|
+
* (profile-context-window §6.1). Optional so every existing construction
|
|
69
|
+
* site stays untouched: a meter built without it reports an overridden
|
|
70
|
+
* window with origin `undefined`, and `/context` falls back to its
|
|
71
|
+
* pre-feature "from contextWindow configuration" wording byte-for-byte.
|
|
72
|
+
*/
|
|
73
|
+
getWindowOverrideOrigin?(): ContextWindowOrigin | undefined;
|
|
65
74
|
getRequestVersion?(): number;
|
|
66
75
|
}
|
|
67
76
|
export type ContextUsageListener = (usage: ContextUsageSnapshot) => void;
|
package/dist/compaction/meter.js
CHANGED
|
@@ -63,6 +63,9 @@ export function toContextUsage(p) {
|
|
|
63
63
|
deltaTokens: p.deltaTokens,
|
|
64
64
|
windowKnown: p.windowKnown,
|
|
65
65
|
windowOverridden: p.windowOverridden,
|
|
66
|
+
...(p.windowOverrideOrigin !== undefined
|
|
67
|
+
? { windowOverrideOrigin: p.windowOverrideOrigin }
|
|
68
|
+
: {}),
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
71
|
/** The reading a meter with nothing measured yet reports. */
|
|
@@ -426,6 +429,10 @@ export class ContextMeter {
|
|
|
426
429
|
window.contextWindow !== last?.contextWindow ||
|
|
427
430
|
window.windowKnown !== last?.windowKnown ||
|
|
428
431
|
window.windowOverridden !== last?.windowOverridden ||
|
|
432
|
+
// ORIGIN IS PART OF THE DIRTY CHECK: a save that keeps the same number
|
|
433
|
+
// but moves it from the legacy key to the bound profile would otherwise
|
|
434
|
+
// leave `/context` naming the old source until the next measurement.
|
|
435
|
+
window.windowOverrideOrigin !== last?.windowOverrideOrigin ||
|
|
429
436
|
this.deps.getModelInfo().contextWindowSource !== last?.windowSource)
|
|
430
437
|
this.dirty = true;
|
|
431
438
|
}
|
|
@@ -451,6 +458,9 @@ export class ContextMeter {
|
|
|
451
458
|
windowSource: this.deps.getModelInfo().contextWindowSource,
|
|
452
459
|
windowKnown: window.windowKnown,
|
|
453
460
|
windowOverridden: window.windowOverridden,
|
|
461
|
+
...(window.windowOverrideOrigin !== undefined
|
|
462
|
+
? { windowOverrideOrigin: window.windowOverrideOrigin }
|
|
463
|
+
: {}),
|
|
454
464
|
...(this.estimateOffset !== undefined ? { estimateOffset: this.estimateOffset } : {}),
|
|
455
465
|
...(this.measuredPrefixLength !== undefined
|
|
456
466
|
? { measuredPrefixLength: this.measuredPrefixLength }
|
|
@@ -469,7 +479,12 @@ export class ContextMeter {
|
|
|
469
479
|
resolveWindow() {
|
|
470
480
|
const override = this.deps.getWindowOverride();
|
|
471
481
|
if (override !== null && Number.isFinite(override) && override > 0) {
|
|
472
|
-
|
|
482
|
+
// ORIGIN ONLY RIDES THE OVERRIDE BRANCH: the AUTO branch names its own
|
|
483
|
+
// source through `getModelInfo().contextWindowSource`, and the two must
|
|
484
|
+
// never both claim the denominator (profile-context-window §6.1).
|
|
485
|
+
const origin = this.deps.getWindowOverrideOrigin?.();
|
|
486
|
+
return { contextWindow: override, windowKnown: true, windowOverridden: true,
|
|
487
|
+
...(origin !== undefined ? { windowOverrideOrigin: origin } : {}) };
|
|
473
488
|
}
|
|
474
489
|
return {
|
|
475
490
|
contextWindow: this.deps.getModelInfo().contextWindow,
|
|
@@ -93,6 +93,13 @@ export interface PressureInput {
|
|
|
93
93
|
* construction site is unchanged and the arithmetic below is untouched.
|
|
94
94
|
*/
|
|
95
95
|
windowOverridden?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Which override surface supplied `contextWindow` (profile-context-window).
|
|
98
|
+
* PURE PASS-THROUGH like `windowOverridden` - the arithmetic below never
|
|
99
|
+
* reads it; it only travels with the number so `/context` can name the
|
|
100
|
+
* source honestly.
|
|
101
|
+
*/
|
|
102
|
+
windowOverrideOrigin?: Pressure['windowOverrideOrigin'];
|
|
96
103
|
/** Carried from the last `turn_end`; applied to every ESTIMATED figure. */
|
|
97
104
|
estimateOffset?: number;
|
|
98
105
|
/**
|
|
@@ -174,6 +174,11 @@ export function computePressure(input) {
|
|
|
174
174
|
// context-usage-gauge-accuracy; the flag only travels with the number so
|
|
175
175
|
// `/context` can name the denominator's source (I-6).
|
|
176
176
|
windowOverridden: input.windowOverridden ?? false,
|
|
177
|
+
// CONDITIONAL SPREAD: with no origin there is no KEY, keeping a
|
|
178
|
+
// feature-off Pressure byte-identical to the pre-feature one (I-6).
|
|
179
|
+
...(input.windowOverrideOrigin !== undefined
|
|
180
|
+
? { windowOverrideOrigin: input.windowOverrideOrigin }
|
|
181
|
+
: {}),
|
|
177
182
|
...(input.estimateOffset !== undefined ? { estimateOffset: input.estimateOffset } : {}),
|
|
178
183
|
deltaTokens,
|
|
179
184
|
};
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* possible change.
|
|
21
21
|
*/
|
|
22
22
|
import type { ModelRef, TokenUsage } from '@aragon-agent/core';
|
|
23
|
+
import type { ContextWindowOrigin } from '../config/schema.js';
|
|
23
24
|
/** Billing belongs to the actual call, including fallback and late responses. */
|
|
24
25
|
export interface CompactionBilling {
|
|
25
26
|
modelRef?: ModelRef;
|
|
@@ -92,6 +93,14 @@ export interface Pressure {
|
|
|
92
93
|
* one being wrong is exactly the case that most needs to be visible.
|
|
93
94
|
*/
|
|
94
95
|
windowOverridden: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* WHICH override surface supplied the denominator (profile-context-window
|
|
98
|
+
* §5.2): env / legacy top-level key / the profile bound as Main. Optional
|
|
99
|
+
* and PURE PASS-THROUGH - present only when `windowOverridden` is true, and
|
|
100
|
+
* `undefined` there means "config", the pre-feature answer. The arithmetic
|
|
101
|
+
* never reads it; only `/context`'s window line does.
|
|
102
|
+
*/
|
|
103
|
+
windowOverrideOrigin?: ContextWindowOrigin;
|
|
95
104
|
/**
|
|
96
105
|
* `occupiedTokens(lastUsage) - estimateOf(the history that produced it)`,
|
|
97
106
|
* clamped to `>= 0`, or `undefined` before the first `turn_end`.
|
|
@@ -150,6 +159,8 @@ export interface ContextUsageSnapshot {
|
|
|
150
159
|
windowKnown: boolean;
|
|
151
160
|
/** Whether the denominator came from the user's `contextWindow` (I-6). */
|
|
152
161
|
windowOverridden: boolean;
|
|
162
|
+
/** Which override surface named the denominator; see `Pressure` (pass-through). */
|
|
163
|
+
windowOverrideOrigin?: ContextWindowOrigin;
|
|
153
164
|
}
|
|
154
165
|
/** One compaction, in whatever state it reached. Rendered by `CompactionCard`. */
|
|
155
166
|
export interface CompactionRecord {
|
|
@@ -105,6 +105,7 @@ export class CompactionWiring {
|
|
|
105
105
|
return source === undefined ? deps.isPricedModel(this.mainRef()) : source !== 'fallback';
|
|
106
106
|
},
|
|
107
107
|
getWindowOverride: () => deps.getConfig().contextWindow,
|
|
108
|
+
getWindowOverrideOrigin: () => deps.getConfig().contextWindowOrigin,
|
|
108
109
|
});
|
|
109
110
|
this.compactor = new Compactor({
|
|
110
111
|
getConfig: deps.getConfig,
|
|
@@ -20,7 +20,7 @@ export declare const LOG_CONFIG_SET_KEYS: readonly ["log.level", "log.toFile", "
|
|
|
20
20
|
* switch falls through every case, writes nothing, and still prints
|
|
21
21
|
* `Set team.enabled = false`.
|
|
22
22
|
*/
|
|
23
|
-
export declare const TEAM_CONFIG_SET_KEYS: readonly ["team.enabled", "team.maxSubagents", "team.maxConcurrent", "team.subagentTimeoutMs", "team.dispatchTimeoutMs", "team.maxTurnsPerSubagent", "team.overseer", "team.overseerIntervalMs"];
|
|
23
|
+
export declare const TEAM_CONFIG_SET_KEYS: readonly ["team.enabled", "team.maxSubagents", "team.maxConcurrent", "team.subagentTimeoutMs", "team.dispatchTimeoutMs", "team.maxTurnsPerSubagent", "team.overseer", "team.overseerIntervalMs", "team.model", "team.modelProfileId"];
|
|
24
24
|
/**
|
|
25
25
|
* Config keys under `retry.` that `aragon config set` accepts
|
|
26
26
|
* (llm-api-retry-backoff §8.2).
|
|
@@ -45,6 +45,8 @@ export const TEAM_CONFIG_SET_KEYS = [
|
|
|
45
45
|
'team.maxTurnsPerSubagent',
|
|
46
46
|
'team.overseer',
|
|
47
47
|
'team.overseerIntervalMs',
|
|
48
|
+
'team.model',
|
|
49
|
+
'team.modelProfileId',
|
|
48
50
|
];
|
|
49
51
|
/**
|
|
50
52
|
* Config keys under `retry.` that `aragon config set` accepts
|
|
@@ -205,6 +207,16 @@ export function applyTeamConfigSet(key, value) {
|
|
|
205
207
|
return patch({ overseer: isTrue(value) });
|
|
206
208
|
case 'team.overseerIntervalMs':
|
|
207
209
|
return patch(clampOne('overseerIntervalMs'));
|
|
210
|
+
// The subagent-model keys (subagent-model-and-question-controls V-3):
|
|
211
|
+
// without these two cases `aragon config set team.model fast` is refused
|
|
212
|
+
// with "Unknown config key" while `config list` shows the key one command
|
|
213
|
+
// over - the exact gap COMPACTION_CONFIG_SET_KEYS records at length.
|
|
214
|
+
// Both route through `clampTeamConfig` like every other key here: a typo'd
|
|
215
|
+
// mode resolves to 'main' and a bad id is nulled, never rejected.
|
|
216
|
+
case 'team.model':
|
|
217
|
+
return patch(clampOne('model'));
|
|
218
|
+
case 'team.modelProfileId':
|
|
219
|
+
return patch(clampOne('modelProfileId'));
|
|
208
220
|
default:
|
|
209
221
|
return null;
|
|
210
222
|
}
|