@saadnvd1/agent-os 0.3.470 → 0.3.471
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -36
- package/app/api/fresh-start/route.ts +9 -5
- package/app/api/usage/route.ts +4 -2
- package/components/Chat/ChatPanel.tsx +5 -5
- package/components/Chat/StaleChip.tsx +7 -6
- package/components/Usage/UsageDialog.tsx +31 -2
- package/components/Workspaces/FreshStartForm.tsx +13 -13
- package/data/usage/index.ts +2 -0
- package/lib/agents/launch.ts +2 -0
- package/lib/chat/refresh.ts +31 -13
- package/lib/chat/stale.ts +25 -16
- package/lib/chat/worker/host.ts +57 -22
- package/lib/db/migrations.ts +42 -0
- package/lib/orchestrator/armed.ts +1 -2
- package/lib/orchestrator/brief.ts +3 -3
- package/lib/orchestrator/claude-cli.ts +5 -0
- package/lib/orchestrator/diff.ts +1 -1
- package/lib/orchestrator/external-gates.ts +8 -43
- package/lib/orchestrator/gates.ts +10 -10
- package/lib/orchestrator/pr-wake.ts +2 -2
- package/lib/orchestrator/rebase.ts +6 -45
- package/lib/orchestrator/review.ts +18 -5
- package/lib/orchestrator/scope.ts +9 -0
- package/lib/orchestrator/signoff.ts +13 -8
- package/lib/session-audit/render.ts +1 -1
- package/lib/stacks/land.ts +23 -16
- package/lib/tasks/brief.ts +3 -14
- package/lib/tasks/code-review.ts +69 -107
- package/lib/tasks/gh.ts +3 -2
- package/lib/tasks/index.ts +1 -1
- package/lib/tasks/state.ts +4 -4
- package/lib/usage/check-runs.ts +167 -0
- package/lib/usage/report.ts +25 -0
- package/lib/usage/turns.ts +28 -0
- package/package.json +1 -1
- package/scripts/check-code-review.ts +0 -21
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ and check on all of it from your phone.
|
|
|
17
17
|
Every workspace has one orchestrator, a chat pinned at the top of its list.
|
|
18
18
|
It starts tasks, answers agents that are blocked, and reviews and merges
|
|
19
19
|
pull requests through gates: CI green and settled, an independent review of
|
|
20
|
-
the exact commit (big diffs are reviewed in parts),
|
|
21
|
-
|
|
20
|
+
the exact commit (big diffs are reviewed in parts), nothing blocked, and the
|
|
21
|
+
diff in scope. Anything that is yours to
|
|
22
22
|
decide (publishing, money, something irreversible, credentials) waits as an
|
|
23
23
|
ask with Approve, Decline and Reply. Turn on **Require my approval** and
|
|
24
24
|
sensitive or large PRs wait for you too. [More](#orchestrator)
|
|
@@ -312,25 +312,34 @@ The ring in the tab bar (in the composer on a phone) is how full the
|
|
|
312
312
|
conversation's context is, measured against the room before auto-compact:
|
|
313
313
|
amber past 60%, red past 85%. Tap it for the breakdown and **Compact now**.
|
|
314
314
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
315
|
+
Chats and tasks compact at 150k tokens of context by default (Settings →
|
|
316
|
+
Workspaces → Compact; off sets it to never), not near the model's window:
|
|
317
|
+
AgentOS launches Claude Code with `CLAUDE_CODE_AUTO_COMPACT_WINDOW` set to
|
|
318
|
+
that size (its floor is 100k), so the agent compacts itself mid-turn. Between
|
|
319
|
+
turns AgentOS catches what it didn't: a chat over the threshold is compacted
|
|
320
|
+
before the next message, and so is one idle past its prompt cache (5 minutes
|
|
321
|
+
or an hour, read from the agent's own usage), whose next message would
|
|
322
|
+
re-read the whole conversation at the cache-write price. A chip above the
|
|
323
|
+
composer says "Idle 47m · 412k context · Compact first?": Send compacts and
|
|
324
|
+
then sends, **Send as-is** skips it for that one message. Messages nobody is
|
|
325
|
+
typing (the orchestrator's `send`, AgentOS events and wakes, `aos send`,
|
|
326
|
+
schedules) compact first on their own, only between turns, and the chat
|
|
327
|
+
shows one quiet line with the context before and after. An orchestrator
|
|
328
|
+
isn't compacted (a summary would lose the fence around what it read from
|
|
329
|
+
tasks and CI): it starts a fresh conversation from its brief and the current
|
|
330
|
+
state at its fresh-start threshold, or earlier once idle past its cache.
|
|
327
331
|
On a desktop browser the screen button attaches a screenshot of a screen or
|
|
328
332
|
window you pick; on a phone, attach a screenshot as a photo.
|
|
329
333
|
|
|
330
334
|
**Usage** (the sidebar's ⋯ menu, or the palette) shows the account's 5-hour
|
|
331
335
|
and weekly windows, and what chats cost today, over 7 or 30 days, by day,
|
|
332
336
|
workspace and session. Costs are the agent's own estimates, recorded as each
|
|
333
|
-
turn ends.
|
|
337
|
+
turn ends. **Gate reviews** below it shows what the orchestrator's
|
|
338
|
+
independent reviewer and scope check cost, by PR: they run as `claude -p`
|
|
339
|
+
and leave no transcript, so each run's tokens, model and duration are read
|
|
340
|
+
from its JSON result into `check_runs`, and the review's verdict says them
|
|
341
|
+
too. Every compaction is kept in `chat_compactions` (who ran it, and the
|
|
342
|
+
context before and after), so cost per task can be compared by date.
|
|
334
343
|
|
|
335
344
|
**Esc** stops a running turn, as it does in Claude Code's terminal, unless a
|
|
336
345
|
menu, dialog or the command list is open (those close first). On a phone the
|
|
@@ -465,17 +474,15 @@ Starting a task returns at once and shows **Setting up** until the agent
|
|
|
465
474
|
launches; a restart in the middle resumes it. A failed setup shows on the task
|
|
466
475
|
and is in the agent's first prompt (redacted, as data).
|
|
467
476
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
[.claude/agents](.claude/agents) and the skill in
|
|
478
|
-
[.claude/skills](.claude/skills/README.md).
|
|
477
|
+
Tasks don't review their own PRs. The orchestrator's independent review of
|
|
478
|
+
the PR's exact head commit is the one review: `sign_off` and a stack's
|
|
479
|
+
**Land** refuse a PR without a passing one, and a PR body with AI
|
|
480
|
+
attribution. A head AgentOS rebased itself is covered by the review of the
|
|
481
|
+
head it rebased while the PR's own patch is unchanged. A task may still run
|
|
482
|
+
`/do-code-review` (the project's `.claude/skills/do-code-review`, or Claude
|
|
483
|
+
Code's `/code-review`) on a risky change before its PR; it's no longer
|
|
484
|
+
required. The review agents are in [.claude/agents](.claude/agents) and the
|
|
485
|
+
skill in [.claude/skills](.claude/skills/README.md).
|
|
479
486
|
|
|
480
487
|
Requires the GitHub CLI (`gh`) signed in, and a project with a GitHub remote.
|
|
481
488
|
|
|
@@ -934,7 +941,7 @@ it may do on its own, and the lines that always come back to you as asks.
|
|
|
934
941
|
it (2 minutes with no new check), its review passed, nothing is
|
|
935
942
|
`BLOCKED:` or waiting, the diff stays in scope (no secrets, not only
|
|
936
943
|
lockfiles, nothing outside the repo, within the card) and its stack parent
|
|
937
|
-
has merged, and the PR body
|
|
944
|
+
has merged, and the PR body carries no AI attribution. `land` judges each item again at its own head right before
|
|
938
945
|
merging it. A gate failing again on a different commit, or a repo with no
|
|
939
946
|
CI, goes to you as an ask, and the orchestrator stops merging that task.
|
|
940
947
|
A failure counts once per task, gate and head commit, however many times
|
|
@@ -953,17 +960,15 @@ it may do on its own, and the lines that always come back to you as asks.
|
|
|
953
960
|
conflicts (read in the PR poll AgentOS already makes), `git merge-tree`
|
|
954
961
|
checks it against the base. Clean, and with the task's agent idle and
|
|
955
962
|
nothing uncommitted, AgentOS rebases it in the task's worktree and pushes
|
|
956
|
-
only its branch with `--force-with-lease
|
|
957
|
-
|
|
958
|
-
and the orchestrator's review carries over when the PR's own patch is
|
|
963
|
+
only its branch with `--force-with-lease`, and the orchestrator's review
|
|
964
|
+
carries over when the PR's own patch is
|
|
959
965
|
unchanged (same `git patch-id`; a fresh review starts otherwise). Not
|
|
960
966
|
clean: the task is asked once to rebase. Either way, one event. Main and
|
|
961
967
|
other tasks' branches are never written.
|
|
962
968
|
- **Tasks woken on CI failures and reviews:** a check failing on a task's
|
|
963
969
|
PR head messages the task's own agent once per head, with the check, the
|
|
964
970
|
failing step and about 60 lines of `gh run view --log-failed` around the
|
|
965
|
-
error, telling it to fix the root cause
|
|
966
|
-
line. A review or comment from a collaborator (not the account gh runs
|
|
971
|
+
error, telling it to fix the root cause and push. A review or comment from a collaborator (not the account gh runs
|
|
967
972
|
as, which is the task's own) wakes it once per review or comment; one
|
|
968
973
|
GraphQL query per repository a minute fingerprints the open task PRs, and
|
|
969
974
|
only a PR whose fingerprint moved has its reviews read. Comment-only wakes
|
|
@@ -976,8 +981,7 @@ it may do on its own, and the lines that always come back to you as asks.
|
|
|
976
981
|
`owner/repo#N` or URL, as long as its repository is one of the workspace's
|
|
977
982
|
projects; anything else is refused. The same gates apply, except that a
|
|
978
983
|
draft waits and a "blocked", "do not merge" or "wip" label holds it (there's
|
|
979
|
-
no session to read)
|
|
980
|
-
the orchestrator's own review of that head. `sessions` also lists tmux
|
|
984
|
+
no session to read). `sessions` also lists tmux
|
|
981
985
|
sessions AgentOS didn't start that work in a workspace repository or a
|
|
982
986
|
worktree of it, here or on another machine, with their branch's PR; `read`
|
|
983
987
|
reads them, and nothing else acts on them.
|
|
@@ -1333,8 +1337,7 @@ until `npm install` runs in them. Typecheck, lint and tests are left to CI.
|
|
|
1333
1337
|
In an emergency, `git commit --no-verify` skips it. CI runs `scripts/check --build` on every pull request and push to main,
|
|
1334
1338
|
which also holds the app's first-load JavaScript to a budget
|
|
1335
1339
|
(`scripts/check-bundle.mjs`; `npx next experimental-analyze` shows what's in
|
|
1336
|
-
it)
|
|
1337
|
-
commit (run `/do-code-review` first; see [Tasks](#tasks)).
|
|
1340
|
+
it).
|
|
1338
1341
|
|
|
1339
1342
|
### Session audits
|
|
1340
1343
|
|
|
@@ -2,17 +2,21 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import { refreshSettings, setRefreshSettings } from "@/lib/chat/refresh";
|
|
3
3
|
|
|
4
4
|
// When a chat's conversation starts over (lib/chat/refresh): context
|
|
5
|
-
// tokens for orchestrators and for tasks, and
|
|
6
|
-
//
|
|
5
|
+
// tokens for orchestrators and for tasks, and at which chats and tasks
|
|
6
|
+
// compact (lib/chat/stale); 0 for never.
|
|
7
7
|
export async function GET() {
|
|
8
8
|
return NextResponse.json(refreshSettings());
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export async function PUT(request: NextRequest) {
|
|
12
12
|
try {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const body = await request.json();
|
|
14
|
+
setRefreshSettings({
|
|
15
|
+
orchestratorTokens: body.orchestratorTokens,
|
|
16
|
+
taskTokens: body.taskTokens,
|
|
17
|
+
// Its name before it also set the agents' own auto-compact.
|
|
18
|
+
compactTokens: body.compactTokens ?? body.staleCompactTokens,
|
|
19
|
+
});
|
|
16
20
|
return NextResponse.json(refreshSettings());
|
|
17
21
|
} catch (error) {
|
|
18
22
|
const message = error instanceof Error ? error.message : String(error);
|
package/app/api/usage/route.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { USAGE_RANGES, type UsageRange } from "@/lib/usage/aggregate";
|
|
3
|
-
import { usageReport } from "@/lib/usage/report";
|
|
3
|
+
import { checkRunReport, usageReport } from "@/lib/usage/report";
|
|
4
4
|
import { usageWindows } from "@/lib/usage/windows";
|
|
5
5
|
|
|
6
6
|
// GET /api/usage?range=today|7d|30d - chat spend by day, session and
|
|
7
|
-
// workspace, and the account's
|
|
7
|
+
// workspace, what the orchestrator's reviews cost by PR, and the account's
|
|
8
|
+
// rolling windows
|
|
8
9
|
export async function GET(request: NextRequest) {
|
|
9
10
|
const asked = request.nextUrl.searchParams.get("range") as UsageRange | null;
|
|
10
11
|
const range = asked && USAGE_RANGES.includes(asked) ? asked : "7d";
|
|
11
12
|
return NextResponse.json({
|
|
12
13
|
report: usageReport(range),
|
|
14
|
+
reviews: checkRunReport(range),
|
|
13
15
|
windows: await usageWindows(),
|
|
14
16
|
});
|
|
15
17
|
}
|
|
@@ -285,9 +285,9 @@ export function ChatPanel({
|
|
|
285
285
|
toEnd();
|
|
286
286
|
send(text);
|
|
287
287
|
};
|
|
288
|
-
// A
|
|
289
|
-
// "Send as-is", for that send only.
|
|
290
|
-
const
|
|
288
|
+
// A chat past the compact threshold compacts before the next message
|
|
289
|
+
// unless the reader picks "Send as-is", for that send only.
|
|
290
|
+
const compactTokens = useFreshStartQuery().data?.compactTokens ?? 0;
|
|
291
291
|
const [asIsFor, setAsIsFor] = useState<string | null>(null);
|
|
292
292
|
const asIs = asIsFor === sessionId;
|
|
293
293
|
const setAsIs = (on: boolean) => setAsIsFor(on ? sessionId : null);
|
|
@@ -529,7 +529,7 @@ export function ChatPanel({
|
|
|
529
529
|
<StaleChip
|
|
530
530
|
context={context}
|
|
531
531
|
state={state}
|
|
532
|
-
thresholdTokens={
|
|
532
|
+
thresholdTokens={compactTokens}
|
|
533
533
|
asIs={asIs}
|
|
534
534
|
onAsIs={setAsIs}
|
|
535
535
|
/>
|
|
@@ -547,7 +547,7 @@ export function ChatPanel({
|
|
|
547
547
|
toEnd();
|
|
548
548
|
const compact = composerCompacts(text, context, {
|
|
549
549
|
state,
|
|
550
|
-
thresholdTokens:
|
|
550
|
+
thresholdTokens: compactTokens,
|
|
551
551
|
asIs,
|
|
552
552
|
});
|
|
553
553
|
setAsIs(false);
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { Minimize2 } from "lucide-react";
|
|
4
4
|
import type { ChatContext, ChatState } from "@/lib/chat/events";
|
|
5
|
-
import {
|
|
5
|
+
import { compactionDue, staleLabel } from "@/lib/chat/stale";
|
|
6
6
|
import { cn } from "@/lib/utils";
|
|
7
7
|
import { useNow } from "./useNow";
|
|
8
8
|
|
|
9
|
-
// Above the composer while the chat is
|
|
10
|
-
//
|
|
11
|
-
// re-read all of it at full price.
|
|
12
|
-
// picks "Send as-is", for that one
|
|
9
|
+
// Above the composer while the chat is due a compaction (lib/chat/stale):
|
|
10
|
+
// its context is past the compact threshold, and maybe its prompt cache has
|
|
11
|
+
// expired too, so the next message would re-read all of it at full price.
|
|
12
|
+
// Send compacts first unless the reader picks "Send as-is", for that one
|
|
13
|
+
// send only.
|
|
13
14
|
export function StaleChip({
|
|
14
15
|
context,
|
|
15
16
|
state,
|
|
@@ -25,7 +26,7 @@ export function StaleChip({
|
|
|
25
26
|
}) {
|
|
26
27
|
const watching = !!context?.cacheTtlMs && thresholdTokens > 0;
|
|
27
28
|
const now = useNow(watching);
|
|
28
|
-
const stale =
|
|
29
|
+
const stale = compactionDue(context, { state, thresholdTokens, now });
|
|
29
30
|
if (!stale) return null;
|
|
30
31
|
return (
|
|
31
32
|
<div
|
|
@@ -41,8 +41,8 @@ export function UsageDialog() {
|
|
|
41
41
|
<DialogHeader>
|
|
42
42
|
<DialogTitle>Usage</DialogTitle>
|
|
43
43
|
<DialogDescription>
|
|
44
|
-
The account's limits, and what chat sessions
|
|
45
|
-
cost.
|
|
44
|
+
The account's limits, and what chat sessions and gate reviews
|
|
45
|
+
on this machine cost.
|
|
46
46
|
</DialogDescription>
|
|
47
47
|
</DialogHeader>
|
|
48
48
|
|
|
@@ -124,6 +124,35 @@ export function UsageDialog() {
|
|
|
124
124
|
</>
|
|
125
125
|
)}
|
|
126
126
|
</section>
|
|
127
|
+
|
|
128
|
+
{data?.reviews && data.reviews.total.turns > 0 && (
|
|
129
|
+
<section className="space-y-3">
|
|
130
|
+
<h3 className="text-sm font-medium">Gate reviews</h3>
|
|
131
|
+
<div className="flex items-baseline gap-2">
|
|
132
|
+
<span className="text-2xl font-semibold tabular-nums">
|
|
133
|
+
{formatUsd(data.reviews.total.costUsd)}
|
|
134
|
+
</span>
|
|
135
|
+
<span className="text-muted-foreground text-xs">
|
|
136
|
+
estimate · {data.reviews.total.turns} runs ·{" "}
|
|
137
|
+
{data.reviews.total.tokens.toLocaleString()} tokens
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
<SpendList
|
|
141
|
+
title="By PR"
|
|
142
|
+
rows={data.reviews.sessions.map((s) => ({
|
|
143
|
+
key: s.id,
|
|
144
|
+
label: s.name,
|
|
145
|
+
...s,
|
|
146
|
+
}))}
|
|
147
|
+
total={data.reviews.total}
|
|
148
|
+
limit={10}
|
|
149
|
+
/>
|
|
150
|
+
<p className="text-muted-foreground text-[11px]">
|
|
151
|
+
Each run of the orchestrator's independent reviewer and scope
|
|
152
|
+
check, from its own result. Not part of chat spend.
|
|
153
|
+
</p>
|
|
154
|
+
</section>
|
|
155
|
+
)}
|
|
127
156
|
</DialogContent>
|
|
128
157
|
</Dialog>
|
|
129
158
|
);
|
|
@@ -19,7 +19,7 @@ export function FreshStartForm() {
|
|
|
19
19
|
return <p className="text-destructive text-sm">{error.message}</p>;
|
|
20
20
|
return (
|
|
21
21
|
<Form
|
|
22
|
-
key={`${data.orchestratorTokens}:${data.taskTokens}:${data.
|
|
22
|
+
key={`${data.orchestratorTokens}:${data.taskTokens}:${data.compactTokens}`}
|
|
23
23
|
saved={data}
|
|
24
24
|
/>
|
|
25
25
|
);
|
|
@@ -29,13 +29,13 @@ function Form({ saved }: { saved: RefreshSettings }) {
|
|
|
29
29
|
const update = useUpdateFreshStart();
|
|
30
30
|
const orchestrator = useThreshold(saved.orchestratorTokens, 200);
|
|
31
31
|
const task = useThreshold(saved.taskTokens, 200);
|
|
32
|
-
const
|
|
33
|
-
const error = orchestrator.error ?? task.error ??
|
|
32
|
+
const compact = useThreshold(saved.compactTokens, 150);
|
|
33
|
+
const error = orchestrator.error ?? task.error ?? compact.error;
|
|
34
34
|
const dirty =
|
|
35
35
|
!error &&
|
|
36
36
|
(orchestrator.tokens !== saved.orchestratorTokens ||
|
|
37
37
|
task.tokens !== saved.taskTokens ||
|
|
38
|
-
|
|
38
|
+
compact.tokens !== saved.compactTokens);
|
|
39
39
|
|
|
40
40
|
return (
|
|
41
41
|
<form
|
|
@@ -46,7 +46,7 @@ function Form({ saved }: { saved: RefreshSettings }) {
|
|
|
46
46
|
{
|
|
47
47
|
orchestratorTokens: orchestrator.tokens,
|
|
48
48
|
taskTokens: task.tokens,
|
|
49
|
-
|
|
49
|
+
compactTokens: compact.tokens,
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
onSuccess: () => toast.success("Context settings saved"),
|
|
@@ -65,17 +65,17 @@ function Form({ saved }: { saved: RefreshSettings }) {
|
|
|
65
65
|
</p>
|
|
66
66
|
<ThresholdRow label="Orchestrators" field={orchestrator} />
|
|
67
67
|
<ThresholdRow label="Tasks" field={task} />
|
|
68
|
-
<h3 className="pt-2 text-sm font-medium">Compact
|
|
68
|
+
<h3 className="pt-2 text-sm font-medium">Compact</h3>
|
|
69
69
|
<p className="text-muted-foreground text-sm">
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
to.
|
|
70
|
+
Chats and tasks compact when their context passes this size: Claude Code
|
|
71
|
+
does it itself, and between turns AgentOS compacts a chat over it, or
|
|
72
|
+
idle past its prompt cache, before the next message. The composer offers
|
|
73
|
+
to first. Orchestrators start fresh instead.
|
|
74
74
|
</p>
|
|
75
75
|
<ThresholdRow
|
|
76
|
-
label="
|
|
77
|
-
field={
|
|
78
|
-
switchLabel="Compact
|
|
76
|
+
label="Chats and tasks"
|
|
77
|
+
field={compact}
|
|
78
|
+
switchLabel="Compact chats and tasks at this size"
|
|
79
79
|
/>
|
|
80
80
|
{error && <p className="text-destructive text-xs">{error}</p>}
|
|
81
81
|
<div className="flex justify-end">
|
package/data/usage/index.ts
CHANGED
package/lib/agents/launch.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { runInBackground } from "../async-operations";
|
|
|
9
9
|
import { trustPromptKeys } from "../tasks/state";
|
|
10
10
|
import { BUS_BRIEF } from "./brief";
|
|
11
11
|
import { selfUrl } from "./self-env";
|
|
12
|
+
import { autoCompactEnv } from "../chat/refresh";
|
|
12
13
|
import { CLAUDE_STATUS_SETTINGS_FLAG } from "../program-status/claude-flag";
|
|
13
14
|
import {
|
|
14
15
|
sessionProjectEnv,
|
|
@@ -35,6 +36,7 @@ export function ensureBusBrief(): void {
|
|
|
35
36
|
export function agentEnv(sessionId: string): Record<string, string> {
|
|
36
37
|
return {
|
|
37
38
|
...sessionProjectEnv(sessionId),
|
|
39
|
+
...autoCompactEnv(sessionId),
|
|
38
40
|
AGENTOS_SESSION_ID: sessionId,
|
|
39
41
|
AGENTOS_URL: selfUrl(),
|
|
40
42
|
PATH: `${AOS_BIN_DIR}:${process.env.PATH ?? ""}`,
|
package/lib/chat/refresh.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* default: its state is in the database and its brief carries it
|
|
7
7
|
* (lib/orchestrator/digest). A task keeps its progress only in its
|
|
8
8
|
* conversation and its branch, so for tasks it's off unless switched on.
|
|
9
|
-
* App-wide, in Settings → Workspaces, with the size
|
|
10
|
-
*
|
|
9
|
+
* App-wide, in Settings → Workspaces, with the size at which chats and
|
|
10
|
+
* tasks compact (lib/chat/stale).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { db } from "../db";
|
|
@@ -15,18 +15,20 @@ import { MAX_REFRESH_TOKENS, MIN_REFRESH_TOKENS } from "./refresh-limits";
|
|
|
15
15
|
|
|
16
16
|
export const ORCHESTRATOR_KEY = "orchestrator.refresh_tokens";
|
|
17
17
|
export const TASK_KEY = "chat.task_refresh_tokens";
|
|
18
|
-
|
|
18
|
+
// Named for what it first did: compact chats idle past their cache.
|
|
19
|
+
export const COMPACT_KEY = "chat.stale_compact_tokens";
|
|
19
20
|
export const DEFAULT_ORCHESTRATOR_TOKENS = 200_000;
|
|
20
|
-
export const
|
|
21
|
+
export const DEFAULT_COMPACT_TOKENS = 150_000;
|
|
21
22
|
export { MIN_REFRESH_TOKENS, MAX_REFRESH_TOKENS } from "./refresh-limits";
|
|
22
23
|
|
|
23
24
|
export interface RefreshSettings {
|
|
24
25
|
// Context tokens at which a conversation starts over; 0 is off.
|
|
25
26
|
orchestratorTokens: number;
|
|
26
27
|
taskTokens: number;
|
|
27
|
-
// Context tokens
|
|
28
|
-
//
|
|
29
|
-
|
|
28
|
+
// Context tokens at which a chat's or task's agent compacts: Claude
|
|
29
|
+
// Code's own auto-compact window, and between turns a chat over it (or
|
|
30
|
+
// idle past its cache) is compacted before the next message. 0 is off.
|
|
31
|
+
compactTokens: number;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
function read(key: string, fallback: number): number {
|
|
@@ -41,7 +43,7 @@ export function refreshSettings(): RefreshSettings {
|
|
|
41
43
|
return {
|
|
42
44
|
orchestratorTokens: read(ORCHESTRATOR_KEY, DEFAULT_ORCHESTRATOR_TOKENS),
|
|
43
45
|
taskTokens: read(TASK_KEY, 0),
|
|
44
|
-
|
|
46
|
+
compactTokens: read(COMPACT_KEY, DEFAULT_COMPACT_TOKENS),
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -74,16 +76,32 @@ export function setRefreshSettings(input: Partial<RefreshSettings>): void {
|
|
|
74
76
|
]);
|
|
75
77
|
if (input.taskTokens !== undefined)
|
|
76
78
|
checked.push([TASK_KEY, checkRefreshTokens(input.taskTokens)]);
|
|
77
|
-
if (input.
|
|
78
|
-
checked.push([
|
|
79
|
-
STALE_COMPACT_KEY,
|
|
80
|
-
checkRefreshTokens(input.staleCompactTokens),
|
|
81
|
-
]);
|
|
79
|
+
if (input.compactTokens !== undefined)
|
|
80
|
+
checked.push([COMPACT_KEY, checkRefreshTokens(input.compactTokens)]);
|
|
82
81
|
db.transaction(() => {
|
|
83
82
|
for (const [key, n] of checked) put.run(key, String(n));
|
|
84
83
|
})();
|
|
85
84
|
}
|
|
86
85
|
|
|
86
|
+
// Claude Code's own auto-compact window, in plain tokens (it reads "150k"
|
|
87
|
+
// as 150 and clamps anything under 100k up to it).
|
|
88
|
+
export const AUTO_COMPACT_ENV = "CLAUDE_CODE_AUTO_COMPACT_WINDOW";
|
|
89
|
+
const AUTO_COMPACT_MIN = 100_000;
|
|
90
|
+
|
|
91
|
+
// What an agent is launched with so it compacts itself at the threshold.
|
|
92
|
+
// Not an orchestrator: it starts over from its brief at its own threshold
|
|
93
|
+
// instead, since a summary would carry what it read from tasks and CI
|
|
94
|
+
// without its untrusted fence. Nothing when it's off, so a window set in
|
|
95
|
+
// the user's own environment still applies.
|
|
96
|
+
export function autoCompactEnv(sessionId: string): Record<string, string> {
|
|
97
|
+
const row = db
|
|
98
|
+
.prepare(`SELECT role FROM sessions WHERE id = ?`)
|
|
99
|
+
.get(sessionId) as { role: string | null } | undefined;
|
|
100
|
+
const tokens = refreshSettings().compactTokens;
|
|
101
|
+
if (row?.role === "orchestrator" || tokens <= 0) return {};
|
|
102
|
+
return { [AUTO_COMPACT_ENV]: String(Math.max(AUTO_COMPACT_MIN, tokens)) };
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
const k = (tokens: number) => `${Math.round(tokens / 1000)}k`;
|
|
88
106
|
|
|
89
107
|
// What the agent reads ahead of its first message in the new conversation.
|
package/lib/chat/stale.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Compacting between turns. A chat's agent compacts itself at the compact
|
|
3
|
+
* threshold (Claude Code's auto-compact window, set at launch by
|
|
4
|
+
* lib/agents/launch). Between turns AgentOS catches what it didn't: a chat
|
|
5
|
+
* over the threshold is compacted before the next message, and so is one
|
|
6
|
+
* whose prompt cache has expired, since that message would re-read the
|
|
7
|
+
* whole history at the cache-write price (one summary turn, then a small
|
|
8
|
+
* cache write instead of a huge one). The one check every send path uses:
|
|
9
|
+
* the composer to offer it (components/Chat/StaleChip), the chat worker to
|
|
10
|
+
* do it for automated sends (lib/chat/worker/host).
|
|
8
11
|
*
|
|
9
12
|
* Pure, so the browser and the worker read the same rule.
|
|
10
13
|
*/
|
|
@@ -38,21 +41,26 @@ export function cacheTtlMs(
|
|
|
38
41
|
export interface Staleness {
|
|
39
42
|
idleMs: number;
|
|
40
43
|
tokens: number;
|
|
44
|
+
// Idle past the cache its last turn wrote, not only over the threshold.
|
|
45
|
+
stale: boolean;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
export function
|
|
48
|
+
// Due a compaction before the next message: no turn running and the
|
|
49
|
+
// context above the threshold (0 is off). Null when it isn't, or when the
|
|
50
|
+
// chat never said which cache it uses (only Claude's chats do).
|
|
51
|
+
export function compactionDue(
|
|
47
52
|
context: ChatContext | null | undefined,
|
|
48
53
|
opts: { state: ChatState; thresholdTokens: number; now?: number }
|
|
49
54
|
): Staleness | null {
|
|
50
55
|
if (opts.state === "running" || opts.state === "waiting") return null;
|
|
51
56
|
if (!context?.cacheTtlMs || opts.thresholdTokens <= 0) return null;
|
|
52
|
-
const idleMs = (opts.now ?? Date.now()) - context.at;
|
|
53
|
-
if (idleMs <= context.cacheTtlMs) return null;
|
|
54
57
|
if (context.usedTokens <= opts.thresholdTokens) return null;
|
|
55
|
-
|
|
58
|
+
const idleMs = (opts.now ?? Date.now()) - context.at;
|
|
59
|
+
return {
|
|
60
|
+
idleMs,
|
|
61
|
+
tokens: context.usedTokens,
|
|
62
|
+
stale: idleMs > context.cacheTtlMs,
|
|
63
|
+
};
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
export function formatIdle(ms: number): string {
|
|
@@ -62,9 +70,10 @@ export function formatIdle(ms: number): string {
|
|
|
62
70
|
return h < 48 ? `${h}h` : `${Math.floor(h / 24)}d`;
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
// "Idle 47m · 412k context"
|
|
73
|
+
// "Idle 47m · 412k context", or "412k context" while its cache is warm.
|
|
66
74
|
export function staleLabel(s: Staleness): string {
|
|
67
|
-
|
|
75
|
+
const size = `${formatTokens(s.tokens)} context`;
|
|
76
|
+
return s.stale ? `Idle ${formatIdle(s.idleMs)} · ${size}` : size;
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
// Whether the composer's send compacts first: the chip was showing and the
|
|
@@ -75,7 +84,7 @@ export function composerCompacts(
|
|
|
75
84
|
opts: { state: ChatState; thresholdTokens: number; asIs: boolean }
|
|
76
85
|
): boolean {
|
|
77
86
|
if (opts.asIs || text.trimStart().startsWith("/")) return false;
|
|
78
|
-
return !!
|
|
87
|
+
return !!compactionDue(context, opts);
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
// The chat's line for a compaction: "Compacted before sending · idle 47m ·
|