feat(ai-chat): durable detached agent runs — phase 1 (#184/#234)
Squashed for a clean rebase onto develop (was 19 commits; the reviewer approved
the net diff at fb246080). Detaches an agent run from the HTTP request/browser
window: a run is a first-class lifecycle object (ai_chat_runs), a browser
disconnect no longer kills it, a concurrent-run insert-gate prevents double runs,
and a reopened chat live-follows a still-running run via a polled observer merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+62
@@ -394,6 +394,10 @@ export default function AiProviderSettings() {
|
||||
useState<boolean>(
|
||||
workspace?.settings?.ai?.publicShareAssistant ?? false,
|
||||
);
|
||||
// #184: detached/autonomous agent runs (settings.ai.autonomousRuns).
|
||||
const [autonomousRunsEnabled, setAutonomousRunsEnabled] = useState<boolean>(
|
||||
workspace?.settings?.ai?.autonomousRuns ?? false,
|
||||
);
|
||||
const [chatToggleLoading, setChatToggleLoading] = useState(false);
|
||||
const [searchToggleLoading, setSearchToggleLoading] = useState(false);
|
||||
const [dictationToggleLoading, setDictationToggleLoading] = useState(false);
|
||||
@@ -403,6 +407,8 @@ export default function AiProviderSettings() {
|
||||
publicShareAssistantToggleLoading,
|
||||
setPublicShareAssistantToggleLoading,
|
||||
] = useState(false);
|
||||
const [autonomousRunsToggleLoading, setAutonomousRunsToggleLoading] =
|
||||
useState(false);
|
||||
|
||||
// Whether a key is currently stored server-side (drives the placeholder).
|
||||
const [hasApiKey, setHasApiKey] = useState(false);
|
||||
@@ -730,6 +736,37 @@ export default function AiProviderSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// Optimistic toggle for detached/autonomous agent runs
|
||||
// (settings.ai.autonomousRuns). When on, a chat turn becomes a server-side run
|
||||
// that survives a browser disconnect and can be reconnected to / live-followed;
|
||||
// only an explicit Stop ends it. Off by default; single-instance-only in phase 1.
|
||||
async function handleToggleAutonomousRuns(value: boolean) {
|
||||
setAutonomousRunsToggleLoading(true);
|
||||
const previous = autonomousRunsEnabled;
|
||||
setAutonomousRunsEnabled(value);
|
||||
try {
|
||||
const updated = await updateWorkspace({ autonomousRuns: value });
|
||||
setWorkspace({
|
||||
...updated,
|
||||
settings: {
|
||||
...updated.settings,
|
||||
ai: { ...updated.settings?.ai, autonomousRuns: value },
|
||||
},
|
||||
});
|
||||
notifications.show({ message: t("Updated successfully") });
|
||||
} catch (err) {
|
||||
setAutonomousRunsEnabled(previous);
|
||||
const message = (err as { response?: { data?: { message?: string } } })
|
||||
?.response?.data?.message;
|
||||
notifications.show({
|
||||
message: message ?? t("Failed to update data"),
|
||||
color: "red",
|
||||
});
|
||||
} finally {
|
||||
setAutonomousRunsToggleLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Admins only — match the previous behavior.
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
@@ -960,6 +997,31 @@ export default function AiProviderSettings() {
|
||||
{...form.getInputProps("publicShareAssistantRoleId")}
|
||||
/>
|
||||
|
||||
{/* Detached/autonomous agent runs: a chat turn becomes a server-side run
|
||||
that survives a browser disconnect; only an explicit Stop ends it.
|
||||
Single-instance-only in phase 1. */}
|
||||
<Group justify="space-between" align="center" wrap="nowrap" mt="md">
|
||||
<Stack gap={0}>
|
||||
<Text fw={600} size="sm">
|
||||
{t("Autonomous agent runs")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t(
|
||||
"Keep an agent turn running server-side even if the browser disconnects; reconnect and follow it on reopen. Single-instance deployments only.",
|
||||
)}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Switch
|
||||
label={t("Enabled")}
|
||||
labelPosition="left"
|
||||
checked={autonomousRunsEnabled}
|
||||
disabled={autonomousRunsToggleLoading}
|
||||
onChange={(e) =>
|
||||
handleToggleAutonomousRuns(e.currentTarget.checked)
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group mt="md" align="center">
|
||||
<Button
|
||||
variant="default"
|
||||
|
||||
@@ -26,6 +26,9 @@ export interface IWorkspace {
|
||||
aiDictation?: boolean;
|
||||
aiDictationStreaming?: boolean;
|
||||
aiPublicShareAssistant?: boolean;
|
||||
// Write-only field for updateWorkspace({ autonomousRuns }). Read state lives at
|
||||
// settings.ai.autonomousRuns.
|
||||
autonomousRuns?: boolean;
|
||||
trashRetentionDays?: number;
|
||||
// Default lifetime (HOURS) for new temporary notes; frozen per-note at creation.
|
||||
temporaryNoteHours?: number;
|
||||
@@ -65,6 +68,9 @@ export interface IWorkspaceAiSettings {
|
||||
dictation?: boolean;
|
||||
dictationStreaming?: boolean;
|
||||
publicShareAssistant?: boolean;
|
||||
// #184: detached agent runs (a run survives a browser disconnect and can be
|
||||
// reconnected to / live-followed on reopen). Gates the run-reconnect polling.
|
||||
autonomousRuns?: boolean;
|
||||
}
|
||||
|
||||
export interface IWorkspaceSharingSettings {
|
||||
|
||||
Reference in New Issue
Block a user