@saadnvd1/agent-os 0.3.493 → 0.3.494
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 +6 -0
- package/app/api/sessions/[id]/fresh/route.ts +31 -0
- package/components/Chat/ChatPanel.tsx +11 -1
- package/components/Pane/index.tsx +4 -0
- package/components/SessionList/OrchestratorPinRow.test.ts +1 -0
- package/components/SessionList/RowContext.tsx +1 -0
- package/components/SessionList/SessionRowMenu.tsx +10 -0
- package/components/SessionList/index.tsx +19 -3
- package/data/sessions/fresh.ts +20 -0
- package/data/sessions/index.ts +1 -0
- package/lib/chat/commands.ts +16 -0
- package/lib/chat/refresh.ts +18 -0
- package/lib/chat/registry.ts +2 -0
- package/lib/chat/runner.ts +50 -0
- package/lib/chat/worker/host.ts +35 -9
- package/lib/chat/worker/main.ts +1 -1
- package/lib/chat/worker/protocol.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1221,6 +1221,12 @@ it may do on its own, and the lines that always come back to you as asks.
|
|
|
1221
1221
|
Nothing is lost, because its state lives in the database; its chat goes on,
|
|
1222
1222
|
with a line where it started over. The same can be switched on for chat
|
|
1223
1223
|
tasks (off by default: a task keeps only its brief and its branch).
|
|
1224
|
+
**Start fresh by hand:** after a deploy that changes its permissions or
|
|
1225
|
+
brief, use **New conversation (keep brief)** in the orchestrator's row menu or type `/fresh`
|
|
1226
|
+
in its chat (task chats have both too). It takes the threshold's path: an
|
|
1227
|
+
idle agent starts over at once; a running turn finishes first, with a
|
|
1228
|
+
"Starts fresh after this turn" line. The next conversation launches with
|
|
1229
|
+
the brief, permissions and workspace state the server has then.
|
|
1224
1230
|
- **Events:** the server sends it one short line per event (a PR opened, CI
|
|
1225
1231
|
failed, or green once it has settled so `sign_off`'s ci gate passes, a `BLOCKED:` line, a merge, a session needing input, a
|
|
1226
1232
|
stack step, a task idle 30 minutes with no PR, a review's verdict). Events
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { queries, db, type Session } from "@/lib/db";
|
|
3
|
+
import { FreshRefusal, freshStartChat } from "@/lib/chat/runner";
|
|
4
|
+
|
|
5
|
+
// POST /api/sessions/:id/fresh - start the agent's conversation over (the
|
|
6
|
+
// path the context threshold takes, lib/chat/refresh): now if it's idle,
|
|
7
|
+
// else at the next turn boundary. Only for chats whose state lives outside
|
|
8
|
+
// the conversation: an orchestrator, or a task with a brief.
|
|
9
|
+
export async function POST(
|
|
10
|
+
_request: Request,
|
|
11
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
12
|
+
) {
|
|
13
|
+
const { id } = await params;
|
|
14
|
+
const session = queries.getSession(db).get(id) as Session | undefined;
|
|
15
|
+
if (!session)
|
|
16
|
+
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
17
|
+
if (session.role !== "orchestrator" && !session.task_brief)
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
{ error: "Only an orchestrator or a task can start fresh" },
|
|
20
|
+
{ status: 400 }
|
|
21
|
+
);
|
|
22
|
+
try {
|
|
23
|
+
return NextResponse.json({ when: await freshStartChat(id) });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
return NextResponse.json(
|
|
27
|
+
{ error: message },
|
|
28
|
+
{ status: error instanceof FreshRefusal ? 409 : 500 }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
type TimelineBlock,
|
|
20
20
|
} from "@/lib/chat/group";
|
|
21
21
|
import type { ApprovalDecision, ChatItem } from "@/lib/chat/events";
|
|
22
|
+
import { isFreshCommand, withFresh } from "@/lib/chat/commands";
|
|
23
|
+
import { useFreshStart } from "@/data/sessions";
|
|
22
24
|
import {
|
|
23
25
|
escapeAction,
|
|
24
26
|
GHOST_SELECTOR,
|
|
@@ -206,6 +208,7 @@ export function ChatPanel({
|
|
|
206
208
|
sessionName,
|
|
207
209
|
accessLocked = false,
|
|
208
210
|
orchestratorOf,
|
|
211
|
+
freshStart = false,
|
|
209
212
|
active,
|
|
210
213
|
}: {
|
|
211
214
|
sessionId: string;
|
|
@@ -214,6 +217,8 @@ export function ChatPanel({
|
|
|
214
217
|
accessLocked?: boolean;
|
|
215
218
|
// The workspace it orchestrates: its header line, Pause and asks show on top.
|
|
216
219
|
orchestratorOf?: string | null;
|
|
220
|
+
// An orchestrator or task: /fresh starts its conversation over.
|
|
221
|
+
freshStart?: boolean;
|
|
217
222
|
// The tab on show in the focused pane: opening it takes the caret.
|
|
218
223
|
active: boolean;
|
|
219
224
|
}) {
|
|
@@ -259,6 +264,7 @@ export function ChatPanel({
|
|
|
259
264
|
requestFiles,
|
|
260
265
|
search,
|
|
261
266
|
} = useChat(sessionId, (text) => setPrefill({ text, at: Date.now() }));
|
|
267
|
+
const startFresh = useFreshStart();
|
|
262
268
|
const { isMobile } = useViewport();
|
|
263
269
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
264
270
|
const listRef = useRef<HTMLDivElement>(null);
|
|
@@ -580,6 +586,10 @@ export function ChatPanel({
|
|
|
580
586
|
disabled={!connected}
|
|
581
587
|
placeholder={connected ? `Message ${sessionName}` : "Reconnecting…"}
|
|
582
588
|
onSend={(text, images) => {
|
|
589
|
+
if (freshStart && isFreshCommand(text)) {
|
|
590
|
+
startFresh.mutate(sessionId);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
583
593
|
toEnd();
|
|
584
594
|
const compact = composerCompacts(text, context, {
|
|
585
595
|
state,
|
|
@@ -590,7 +600,7 @@ export function ChatPanel({
|
|
|
590
600
|
send(text, images.length ? images : undefined, compact);
|
|
591
601
|
}}
|
|
592
602
|
onStop={interrupt}
|
|
593
|
-
commands={commands}
|
|
603
|
+
commands={freshStart ? withFresh(commands) : commands}
|
|
594
604
|
onRefreshCommands={refreshCommands}
|
|
595
605
|
refreshingCommands={refreshingCommands}
|
|
596
606
|
models={models}
|
|
@@ -289,6 +289,10 @@ export const Pane = memo(function Pane({
|
|
|
289
289
|
sessionId={chat.id}
|
|
290
290
|
sessionName={chat.name}
|
|
291
291
|
accessLocked={chat.role === "orchestrator"}
|
|
292
|
+
freshStart={
|
|
293
|
+
(chat.role === "orchestrator" || !!chat.task_brief) &&
|
|
294
|
+
!chat.archived_at
|
|
295
|
+
}
|
|
292
296
|
orchestratorOf={
|
|
293
297
|
chat.role === "orchestrator" ? chat.workspace_id : null
|
|
294
298
|
}
|
|
@@ -29,6 +29,7 @@ export interface RowContextValue {
|
|
|
29
29
|
onDelete: (sessionId: string) => void;
|
|
30
30
|
onMoveToProject: (sessionId: string, projectId: string) => void;
|
|
31
31
|
onPin: (sessionId: string, pinned: boolean) => void;
|
|
32
|
+
onFresh: (sessionId: string) => void;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
const RowContext = createContext<RowContextValue | null>(null);
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
Pencil,
|
|
16
16
|
Pin,
|
|
17
17
|
PinOff,
|
|
18
|
+
RotateCcw,
|
|
18
19
|
Sparkles,
|
|
19
20
|
Trash2,
|
|
20
21
|
} from "lucide-react";
|
|
@@ -174,6 +175,15 @@ export function SessionRowMenu({
|
|
|
174
175
|
{summarizing ? "Summarizing..." : "Fresh start"}
|
|
175
176
|
</Item>
|
|
176
177
|
)}
|
|
178
|
+
{(session.role === "orchestrator" || session.task_brief) &&
|
|
179
|
+
session.view === "chat" &&
|
|
180
|
+
!session.archived_at &&
|
|
181
|
+
!blocked && (
|
|
182
|
+
<Item onClick={() => ctx.onFresh(session.id)}>
|
|
183
|
+
<RotateCcw className={icon} />
|
|
184
|
+
New conversation (keep brief)
|
|
185
|
+
</Item>
|
|
186
|
+
)}
|
|
177
187
|
{session.pr_url && (
|
|
178
188
|
<Item onClick={() => window.open(session.pr_url!, "_blank")}>
|
|
179
189
|
<GitPullRequest className={icon} />
|
|
@@ -4,7 +4,7 @@ import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
|
4
4
|
import { Loader2 } from "lucide-react";
|
|
5
5
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
6
6
|
import { useDocsWorkspace } from "@/data/lumifyhub/docs";
|
|
7
|
-
import { usePinSession } from "@/data/sessions";
|
|
7
|
+
import { useFreshStart, usePinSession } from "@/data/sessions";
|
|
8
8
|
import { useOrchestratorsQuery } from "@/data/orchestrators";
|
|
9
9
|
import { docsUiActions } from "@/stores/docsUi";
|
|
10
10
|
import type { SidebarRow } from "@/lib/sidebar/shelves";
|
|
@@ -47,6 +47,7 @@ export function SessionList({
|
|
|
47
47
|
const mutations = useSessionListMutations({ onSelectSession: onSelect });
|
|
48
48
|
const bulkDelete = useBulkDelete();
|
|
49
49
|
const pin = usePinSession();
|
|
50
|
+
const fresh = useFreshStart();
|
|
50
51
|
const { data: orchestrators = NO_ORCHESTRATORS } = useOrchestratorsQuery();
|
|
51
52
|
const [dialog, setDialog] = useState<SidebarDialog | null>(null);
|
|
52
53
|
const addProject = useAddProject();
|
|
@@ -70,9 +71,23 @@ export function SessionList({
|
|
|
70
71
|
// Rows read their handlers through the latest render, so the context only
|
|
71
72
|
// changes when what rows draw does: a status push redraws the rows it
|
|
72
73
|
// touched, not the whole list.
|
|
73
|
-
const latest = useRef({
|
|
74
|
+
const latest = useRef({
|
|
75
|
+
orderedIds,
|
|
76
|
+
onSelect,
|
|
77
|
+
onOpenInTab,
|
|
78
|
+
mutations,
|
|
79
|
+
pin,
|
|
80
|
+
fresh,
|
|
81
|
+
});
|
|
74
82
|
useLayoutEffect(() => {
|
|
75
|
-
latest.current = {
|
|
83
|
+
latest.current = {
|
|
84
|
+
orderedIds,
|
|
85
|
+
onSelect,
|
|
86
|
+
onOpenInTab,
|
|
87
|
+
mutations,
|
|
88
|
+
pin,
|
|
89
|
+
fresh,
|
|
90
|
+
};
|
|
76
91
|
});
|
|
77
92
|
const canOpenInTab = !!onOpenInTab;
|
|
78
93
|
const handlers = useMemo(
|
|
@@ -92,6 +107,7 @@ export function SessionList({
|
|
|
92
107
|
latest.current.mutations.handleMoveSessionToProject(id, projectId),
|
|
93
108
|
onPin: (id: string, pinned: boolean) =>
|
|
94
109
|
latest.current.pin.mutate({ id, pinned }),
|
|
110
|
+
onFresh: (id: string) => latest.current.fresh.mutate(id),
|
|
95
111
|
}),
|
|
96
112
|
[canOpenInTab]
|
|
97
113
|
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useMutation } from "@tanstack/react-query";
|
|
2
|
+
import { toast } from "sonner";
|
|
3
|
+
|
|
4
|
+
// Starts an orchestrator's or task's conversation over (lib/chat/refresh):
|
|
5
|
+
// now when idle, else after the running turn.
|
|
6
|
+
export function useFreshStart() {
|
|
7
|
+
return useMutation({
|
|
8
|
+
mutationFn: async (id: string): Promise<"now" | "after-turn"> => {
|
|
9
|
+
const res = await fetch(`/api/sessions/${id}/fresh`, { method: "POST" });
|
|
10
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
11
|
+
when?: "now" | "after-turn";
|
|
12
|
+
error?: string;
|
|
13
|
+
};
|
|
14
|
+
if (!res.ok || !body.when)
|
|
15
|
+
throw new Error(body.error ?? "Couldn't start fresh");
|
|
16
|
+
return body.when;
|
|
17
|
+
},
|
|
18
|
+
onError: (e: Error) => toast.error(e.message),
|
|
19
|
+
});
|
|
20
|
+
}
|
package/data/sessions/index.ts
CHANGED
package/lib/chat/commands.ts
CHANGED
|
@@ -48,3 +48,19 @@ export function insertCommand(name: string): string {
|
|
|
48
48
|
export function leadingCommand(text: string): string | null {
|
|
49
49
|
return text.match(/^\/[\w:.-]+(?=\s|$)/)?.[0] ?? null;
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
// AgentOS's own command for an orchestrator or task: starts the agent's
|
|
53
|
+
// conversation over, with today's brief and permissions.
|
|
54
|
+
export const FRESH_COMMAND: ChatCommand = {
|
|
55
|
+
name: "fresh",
|
|
56
|
+
description: "Start a fresh conversation with the current brief",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const isFreshCommand = (text: string): boolean =>
|
|
60
|
+
text.trim().toLowerCase() === "/fresh";
|
|
61
|
+
|
|
62
|
+
export function withFresh(commands: ChatCommand[]): ChatCommand[] {
|
|
63
|
+
return commands.some((c) => c.name === "fresh")
|
|
64
|
+
? commands
|
|
65
|
+
: [FRESH_COMMAND, ...commands];
|
|
66
|
+
}
|
package/lib/chat/refresh.ts
CHANGED
|
@@ -143,8 +143,26 @@ export function autoCompactEnv(sessionId: string): Record<string, string> {
|
|
|
143
143
|
|
|
144
144
|
const k = (tokens: number) => `${Math.round(tokens / 1000)}k`;
|
|
145
145
|
|
|
146
|
+
// Recorded as the size a conversation ended at when the reader asked for it.
|
|
147
|
+
export const REQUESTED_FRESH = -1;
|
|
148
|
+
|
|
149
|
+
// The agent's conversation is let go in the database: the next worker
|
|
150
|
+
// starts a new one with the brief and the permissions the server has then.
|
|
151
|
+
// `tokens` is the size it ended at, or REQUESTED_FRESH.
|
|
152
|
+
export function releaseConversation(sessionId: string, tokens: number): void {
|
|
153
|
+
db.prepare(
|
|
154
|
+
`UPDATE sessions SET claude_session_id = NULL, chat_resume_at = NULL,
|
|
155
|
+
chat_suggestion = NULL, chat_context = NULL, chat_restarted = 0,
|
|
156
|
+
chat_refreshed = ? WHERE id = ?`
|
|
157
|
+
).run(tokens, sessionId);
|
|
158
|
+
}
|
|
159
|
+
|
|
146
160
|
// What the agent reads ahead of its first message in the new conversation.
|
|
147
161
|
export function refreshedNote(role: "orchestrator" | "task", tokens: number) {
|
|
162
|
+
if (tokens === REQUESTED_FRESH)
|
|
163
|
+
return role === "orchestrator"
|
|
164
|
+
? `(AgentOS started you on a fresh conversation at the user's request. Your brief and "Where things stand" are current; carry on from them.)`
|
|
165
|
+
: `(AgentOS started you on a fresh conversation at the user's request. Your brief is above; check git status, git log and your PR to see how far you got, then carry on.)`;
|
|
148
166
|
return role === "orchestrator"
|
|
149
167
|
? `(AgentOS started you on a fresh conversation: the last one passed ${k(tokens)} tokens of context. Your brief and "Where things stand" are current; carry on from them.)`
|
|
150
168
|
: `(AgentOS started you on a fresh conversation: the last one passed ${k(tokens)} tokens of context. Your brief is above; check git status, git log and your PR to see how far you got, then carry on.)`;
|
package/lib/chat/registry.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface Live {
|
|
|
24
24
|
// Whether it keeps the composer's queue and answers @mention lookups
|
|
25
25
|
// (send_now, drain, files); an older worker drops those silently.
|
|
26
26
|
canQueue?: boolean;
|
|
27
|
+
// Whether it takes "fresh" (a fresh start the reader asked for).
|
|
28
|
+
canFresh?: boolean;
|
|
27
29
|
// From an older build and idle: retired once its guess at the next
|
|
28
30
|
// message is in, or this fires.
|
|
29
31
|
retiring?: NodeJS.Timeout;
|
package/lib/chat/runner.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
queuedSessions,
|
|
51
51
|
} from "./queued";
|
|
52
52
|
import { fallbackFileSuggestions } from "./files";
|
|
53
|
+
import { REQUESTED_FRESH, releaseConversation } from "./refresh";
|
|
53
54
|
import { holdsQueue, settingUp } from "../sessions/setup-progress";
|
|
54
55
|
import { firstMessageId, launchPending } from "../tasks/launch-gate";
|
|
55
56
|
import type { ChatItem, FileSuggestion } from "./events";
|
|
@@ -280,6 +281,7 @@ async function ensureLive(sessionId: string, spawn = true): Promise<Live> {
|
|
|
280
281
|
build: hello.build,
|
|
281
282
|
canPlan: !!hello.caps?.includes("plan"),
|
|
282
283
|
canQueue: !!hello.caps?.includes("queue"),
|
|
284
|
+
canFresh: !!hello.caps?.includes("fresh"),
|
|
283
285
|
};
|
|
284
286
|
// From an older build and busy, or running background work: it goes at
|
|
285
287
|
// its next turn boundary, by itself if it can. Messages meanwhile wait
|
|
@@ -716,6 +718,54 @@ function retire(sessionId: string, live: Live, resume = true): void {
|
|
|
716
718
|
if (resume) void resumeQueue(sessionId);
|
|
717
719
|
}
|
|
718
720
|
|
|
721
|
+
// A fresh start the reader asked for, by the path the context threshold
|
|
722
|
+
// takes (the worker's endConversation): "now" when nothing is running, else
|
|
723
|
+
// "after-turn", at the next turn boundary. The next worker reads the brief
|
|
724
|
+
// and permissions as the server has them then, nothing held over.
|
|
725
|
+
export class FreshRefusal extends Error {}
|
|
726
|
+
|
|
727
|
+
export async function freshStartChat(
|
|
728
|
+
sessionId: string
|
|
729
|
+
): Promise<"now" | "after-turn"> {
|
|
730
|
+
const session = getSession(sessionId);
|
|
731
|
+
// A terminal agent's conversation isn't ours to release: it keeps running.
|
|
732
|
+
if (session.view !== "chat" || session.archived_at)
|
|
733
|
+
throw new FreshRefusal("Only an open chat session can start fresh");
|
|
734
|
+
try {
|
|
735
|
+
checkChattable(session);
|
|
736
|
+
} catch (e) {
|
|
737
|
+
throw new FreshRefusal(e instanceof Error ? e.message : String(e));
|
|
738
|
+
}
|
|
739
|
+
const live =
|
|
740
|
+
registry.live.get(sessionId) ??
|
|
741
|
+
(runningWorkers().includes(sessionId)
|
|
742
|
+
? await ensureLive(sessionId, false).catch(() => undefined)
|
|
743
|
+
: undefined);
|
|
744
|
+
if (live) {
|
|
745
|
+
// A worker from before this existed would ignore the command.
|
|
746
|
+
if (live.canFresh) {
|
|
747
|
+
live.worker.command({ type: "fresh" });
|
|
748
|
+
return live.state === "idle" && !live.activity.tasks.size
|
|
749
|
+
? "now"
|
|
750
|
+
: "after-turn";
|
|
751
|
+
}
|
|
752
|
+
if (live.state !== "idle")
|
|
753
|
+
throw new FreshRefusal("Try again when the current turn has ended");
|
|
754
|
+
stopChat(sessionId);
|
|
755
|
+
}
|
|
756
|
+
releaseConversation(sessionId, REQUESTED_FRESH);
|
|
757
|
+
const item: ChatItem = {
|
|
758
|
+
id: `note-refresh-${Date.now()}`,
|
|
759
|
+
kind: "note",
|
|
760
|
+
tone: "note",
|
|
761
|
+
text: `Fresh start: the agent starts a new conversation with its brief${session.role === "orchestrator" ? " and the current state from AgentOS" : ""} at its next message.`,
|
|
762
|
+
createdAt: Date.now(),
|
|
763
|
+
};
|
|
764
|
+
saveItem(sessionId, item);
|
|
765
|
+
emit(sessionId, { type: "item", item });
|
|
766
|
+
return "now";
|
|
767
|
+
}
|
|
768
|
+
|
|
719
769
|
export function stopChat(sessionId: string): void {
|
|
720
770
|
const live = registry.live.get(sessionId);
|
|
721
771
|
if (live) {
|
package/lib/chat/worker/host.ts
CHANGED
|
@@ -41,7 +41,13 @@ import {
|
|
|
41
41
|
visualsTools,
|
|
42
42
|
} from "../../artifacts/tools";
|
|
43
43
|
import type { RoleExtras } from "./extras";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
REQUESTED_FRESH,
|
|
46
|
+
compactThreshold,
|
|
47
|
+
refreshSettings,
|
|
48
|
+
refreshedNote,
|
|
49
|
+
releaseConversation,
|
|
50
|
+
} from "../refresh";
|
|
45
51
|
import { compactContext } from "../compact-context";
|
|
46
52
|
import { COMPACT_BRIEF, COMPACT_INSTRUCTIONS } from "../working-state";
|
|
47
53
|
import { compactionDue, staleLabel, type Staleness } from "../stale";
|
|
@@ -679,11 +685,32 @@ export class ChatHost {
|
|
|
679
685
|
case "retire":
|
|
680
686
|
this.retiring = true;
|
|
681
687
|
return this.retireIfDone();
|
|
688
|
+
case "fresh":
|
|
689
|
+
return this.freshOnRequest();
|
|
682
690
|
case "close":
|
|
683
691
|
return this.close();
|
|
684
692
|
}
|
|
685
693
|
}
|
|
686
694
|
|
|
695
|
+
// A fresh start the reader asked for: the same retire as at the context
|
|
696
|
+
// threshold, so it waits for the turn boundary and says so meanwhile.
|
|
697
|
+
private freshOnRequest(): void {
|
|
698
|
+
if (this.refreshAt || this.closed) return;
|
|
699
|
+
this.refreshAt = REQUESTED_FRESH;
|
|
700
|
+
this.retiring = true;
|
|
701
|
+
this.retireWaited = true;
|
|
702
|
+
this.retireIfDone();
|
|
703
|
+
// Still here: a turn, background work or its rest timer holds it.
|
|
704
|
+
if (!this.closed)
|
|
705
|
+
this.record({
|
|
706
|
+
id: `note-fresh-pending-${Date.now()}`,
|
|
707
|
+
kind: "note",
|
|
708
|
+
tone: "note",
|
|
709
|
+
text: "Starts fresh after this turn",
|
|
710
|
+
createdAt: Date.now(),
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
687
714
|
private hardStopped(): boolean {
|
|
688
715
|
return this.hardStopAt > 0 && Date.now() - this.hardStopAt < HARD_STOP_MS;
|
|
689
716
|
}
|
|
@@ -849,20 +876,19 @@ export class ChatHost {
|
|
|
849
876
|
// with the brief and tells it why. The chat Saad reads goes on, with a
|
|
850
877
|
// line saying where it started over.
|
|
851
878
|
private endConversation(): void {
|
|
852
|
-
|
|
853
|
-
`UPDATE sessions SET claude_session_id = NULL, chat_resume_at = NULL,
|
|
854
|
-
chat_suggestion = NULL, chat_context = NULL, chat_restarted = 0,
|
|
855
|
-
chat_refreshed = ? WHERE id = ?`
|
|
856
|
-
).run(this.refreshAt, this.session.id);
|
|
879
|
+
releaseConversation(this.session.id, this.refreshAt);
|
|
857
880
|
const brief = `its brief${this.session.role === "orchestrator" ? " and the current state from AgentOS" : ""}`;
|
|
858
881
|
const stale = this.staleRefresh;
|
|
882
|
+
const requested = this.refreshAt === REQUESTED_FRESH;
|
|
859
883
|
this.record({
|
|
860
884
|
id: `note-refresh-${Date.now()}`,
|
|
861
885
|
kind: "note",
|
|
862
886
|
tone: "note",
|
|
863
|
-
text:
|
|
864
|
-
?
|
|
865
|
-
:
|
|
887
|
+
text: requested
|
|
888
|
+
? `Fresh start: the agent starts a new conversation with ${brief} at its next message.`
|
|
889
|
+
: stale
|
|
890
|
+
? `${staleLabel(stale)}, past its prompt cache: the agent starts a fresh conversation with ${brief} before the next message.`
|
|
891
|
+
: `Context reached ${Math.round(this.refreshAt / 1000)}k tokens: the agent starts a fresh conversation with ${brief} at its next message.`,
|
|
866
892
|
createdAt: Date.now(),
|
|
867
893
|
});
|
|
868
894
|
if (stale)
|
package/lib/chat/worker/main.ts
CHANGED
|
@@ -75,7 +75,7 @@ async function main(sessionId: string) {
|
|
|
75
75
|
build: buildId(),
|
|
76
76
|
state: host.state,
|
|
77
77
|
streaming: [...host.streaming.values()],
|
|
78
|
-
caps: ["plan", "queue", "retire"],
|
|
78
|
+
caps: ["plan", "queue", "retire", "fresh"],
|
|
79
79
|
} satisfies WorkerEvent)}\n`
|
|
80
80
|
);
|
|
81
81
|
client.on(
|
|
@@ -58,6 +58,10 @@ export type WorkerCommand =
|
|
|
58
58
|
// wait in the queue) and closes at its next turn boundary, so a current
|
|
59
59
|
// worker resumes the conversation with today's tools.
|
|
60
60
|
| { type: "retire" }
|
|
61
|
+
// The reader asked for a fresh conversation: at the next turn boundary
|
|
62
|
+
// (now, if idle) the agent's conversation ends and the next worker
|
|
63
|
+
// starts a new one with today's brief and permissions.
|
|
64
|
+
| { type: "fresh" }
|
|
61
65
|
| { type: "close" };
|
|
62
66
|
|
|
63
67
|
// Worker -> server: what's live right now on connecting, then events.
|