@saadnvd1/agent-os 0.3.794 → 0.3.807
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/app/api/busy/route.ts +2 -2
- package/app/api/chat-views/[sessionId]/route.ts +19 -0
- package/app/api/chat-views/route.test.ts +79 -0
- package/app/api/chat-views/route.ts +8 -0
- package/app/api/tips/route.test.ts +56 -0
- package/app/api/tips/route.ts +45 -0
- package/app/api/updates/health/route.ts +22 -0
- package/app/api/updates/route.test.ts +53 -0
- package/app/api/updates/settings/route.ts +37 -10
- package/app/page.tsx +17 -14
- package/components/Chat/ChatPanel.tsx +74 -59
- package/components/Chat/NewDivider.tsx +15 -0
- package/components/Chat/needs-more-history.test.ts +28 -0
- package/components/Chat/needs-more-history.ts +14 -0
- package/components/Chat/session-link.ts +7 -4
- package/components/Chat/useFind.ts +50 -1
- package/components/Chat/usePosition.ts +363 -0
- package/components/CodeSearchDialog.tsx +90 -0
- package/components/CommandPalette/CommandsBody.tsx +12 -4
- package/components/CommandPalette/useAppCommands.ts +29 -11
- package/components/DevServers/ServerLogsModal.tsx +24 -21
- package/components/Nav/HistoryButtons.tsx +128 -0
- package/components/Pane/MobileTabBar.tsx +3 -0
- package/components/Pane/index.tsx +2 -2
- package/components/Recipes/TimerFields.tsx +2 -1
- package/components/Schedules/ScheduleForm.tsx +1 -1
- package/components/SessionList/useSessionSwitching.ts +3 -2
- package/components/Settings/SettingsDialog.tsx +1 -1
- package/components/Shortcuts/PendingKeys.tsx +19 -0
- package/components/Shortcuts/ShortcutsButton.tsx +22 -0
- package/components/Shortcuts/ShortcutsDialog.tsx +122 -0
- package/components/SidebarFooter.tsx +4 -0
- package/components/Terminal/hooks/useTerminalConnection.ts +4 -3
- package/components/Tips/TipButton.tsx +39 -0
- package/components/Tips/TipDialog.tsx +148 -0
- package/components/Tips/TipHost.tsx +14 -0
- package/components/Tips/TipMedia.tsx +71 -0
- package/components/Tips/index.ts +2 -0
- package/components/Updates/AutoUpdates.tsx +141 -0
- package/components/Updates/UpdateBanner.test.ts +39 -1
- package/components/Updates/UpdateBanner.tsx +31 -5
- package/components/Updates/UpdatesSection.tsx +24 -3
- package/components/views/DesktopView.tsx +7 -9
- package/components/views/MobileView.tsx +2 -8
- package/components/views/lazy.ts +2 -2
- package/content/tips.json +541 -0
- package/content/whats-new.json +56 -0
- package/data/chat-views/index.ts +2 -0
- package/data/chat-views/keys.ts +3 -0
- package/data/chat-views/queries.ts +34 -0
- package/data/sessions/pin.ts +0 -1
- package/data/tips/index.ts +49 -0
- package/data/updates/index.ts +5 -0
- package/hooks/useChatViewsSync.ts +12 -0
- package/hooks/useNavActions.ts +104 -0
- package/hooks/useNavKeys.ts +105 -0
- package/hooks/useReloadWhenUpdated.ts +5 -1
- package/hooks/useSessionUrl.ts +23 -5
- package/lib/bus/index.ts +4 -2
- package/lib/bus/role-alias.ts +4 -2
- package/lib/bus/targets.ts +2 -1
- package/lib/busy.ts +23 -1
- package/lib/chat/escape.ts +8 -4
- package/lib/chat-views/body.ts +41 -0
- package/lib/chat-views/store.ts +67 -0
- package/lib/chat-views/types.ts +20 -0
- package/lib/db/migrations/20261031T1500_chat_views.ts +30 -0
- package/lib/db/migrations.ts +21 -0
- package/lib/keybindings/focus.ts +17 -0
- package/lib/keybindings/keymap.ts +120 -0
- package/lib/keybindings/sequence.ts +64 -0
- package/lib/live/service.ts +1 -1
- package/lib/nav/history-mirror.ts +59 -0
- package/lib/nav/history-writer.ts +109 -0
- package/lib/nav/position.ts +67 -0
- package/lib/nav/recent.ts +34 -0
- package/lib/orchestrator/armed.ts +5 -0
- package/lib/palette/sections.ts +12 -6
- package/lib/recipes/timer.ts +3 -1
- package/lib/schedules/cron.ts +14 -5
- package/lib/schedules/store.ts +3 -2
- package/lib/security/demo.ts +2 -0
- package/lib/session-url.ts +11 -5
- package/lib/tasks/finish.ts +5 -5
- package/lib/tips/conditions.ts +38 -0
- package/lib/tips/index.ts +2 -0
- package/lib/tips/load.ts +26 -0
- package/lib/tips/media.ts +101 -0
- package/lib/tips/pick.ts +35 -0
- package/lib/tips/schema.ts +82 -0
- package/lib/tips/state.ts +26 -0
- package/lib/tips/view.ts +60 -0
- package/lib/updates/auto.ts +149 -0
- package/lib/updates/banner.ts +78 -3
- package/lib/updates/check.ts +64 -8
- package/lib/updates/history.ts +99 -0
- package/lib/updates/run.ts +334 -23
- package/lib/updates/settings.ts +66 -0
- package/lib/updates/view.ts +14 -3
- package/package.json +2 -2
- package/public/tips/.gitkeep +0 -0
- package/public/tips/chat-access-picker-dark.webp +0 -0
- package/public/tips/chat-access-picker.webp +0 -0
- package/public/tips/chat-attach-files-poster.webp +0 -0
- package/public/tips/chat-attach-files.mp4 +0 -0
- package/public/tips/chat-bookmark-dark.webp +0 -0
- package/public/tips/chat-bookmark.webp +0 -0
- package/public/tips/chat-plan-mode-dark.webp +0 -0
- package/public/tips/chat-plan-mode.webp +0 -0
- package/public/tips/chat-search-poster.webp +0 -0
- package/public/tips/chat-search.mp4 +0 -0
- package/public/tips/chat-slash-menu-poster.webp +0 -0
- package/public/tips/chat-slash-menu.mp4 +0 -0
- package/public/tips/chat-tab-suggestion-poster.webp +0 -0
- package/public/tips/chat-tab-suggestion.mp4 +0 -0
- package/public/tips/chat-terminal-handoff-poster.webp +0 -0
- package/public/tips/chat-terminal-handoff.mp4 +0 -0
- package/public/tips/chat-undo-dark.webp +0 -0
- package/public/tips/chat-undo.webp +0 -0
- package/public/tips/export-ai-dark.webp +0 -0
- package/public/tips/export-ai.webp +0 -0
- package/public/tips/net-messages-dark.webp +0 -0
- package/public/tips/net-messages.webp +0 -0
- package/public/tips/recipe-save-dark.webp +0 -0
- package/public/tips/recipe-save.webp +0 -0
- package/public/tips/schedule-first-dark.webp +0 -0
- package/public/tips/schedule-first.webp +0 -0
- package/public/tips/share-check-dark.webp +0 -0
- package/public/tips/share-check.webp +0 -0
- package/public/tips/sidebar-draft-poster.webp +0 -0
- package/public/tips/sidebar-draft.mp4 +0 -0
- package/public/tips/sidebar-groups-dark.webp +0 -0
- package/public/tips/sidebar-groups.webp +0 -0
- package/public/tips/sidebar-load-dark.webp +0 -0
- package/public/tips/sidebar-load.webp +0 -0
- package/public/tips/sidebar-snooze-dark.webp +0 -0
- package/public/tips/sidebar-snooze.webp +0 -0
- package/public/tips/tasks-plan-pieces-dark.webp +0 -0
- package/public/tips/tasks-plan-pieces.webp +0 -0
- package/public/tips/tasks-sign-off-dark.webp +0 -0
- package/public/tips/tasks-sign-off.webp +0 -0
- package/public/tips/tasks-stacks-dark.webp +0 -0
- package/public/tips/tasks-stacks.webp +0 -0
- package/scripts/autodeploy +4 -0
- package/scripts/autodeploy.test.ts +12 -0
- package/scripts/check +2 -2
- package/scripts/docs-links.test.ts +1 -25
- package/scripts/lib/commands.sh +32 -1
- package/scripts/lib/deps-swap.sh +8 -6
- package/scripts/lib/heading-slugs.ts +26 -0
- package/scripts/lib/update-verify.mjs +90 -0
- package/scripts/redeploy +31 -2
- package/scripts/redeploy.test.ts +94 -3
- package/scripts/tips-lint.test.ts +77 -0
- package/scripts/tips-media.ts +31 -0
- package/scripts/update-e2e.test.ts +215 -0
- package/scripts/update-recover.mjs +130 -0
- package/scripts/update-recover.test.ts +144 -0
- package/scripts/update-run.mjs +182 -40
- package/scripts/update-run.test.ts +208 -2
- package/server.ts +3 -2
- package/stores/chatJump.ts +2 -6
- package/stores/chatPositions.ts +65 -0
- package/stores/navHistory.ts +98 -0
- package/stores/shortcutsUi.ts +20 -0
- package/stores/tips.ts +14 -0
- package/components/QuickSwitcher.tsx +0 -286
package/lib/updates/banner.ts
CHANGED
|
@@ -3,22 +3,33 @@
|
|
|
3
3
|
|
|
4
4
|
export type UpdatePhase =
|
|
5
5
|
| "starting"
|
|
6
|
+
| "backing_up"
|
|
6
7
|
| "pulling"
|
|
7
8
|
| "installing"
|
|
8
9
|
| "building"
|
|
9
10
|
| "restarting"
|
|
11
|
+
| "verifying"
|
|
12
|
+
| "rolling_back"
|
|
10
13
|
| "done"
|
|
11
14
|
| "built"
|
|
15
|
+
| "rolled_back"
|
|
12
16
|
| "failed";
|
|
13
17
|
|
|
14
18
|
export const ACTIVE_PHASES: readonly UpdatePhase[] = [
|
|
15
19
|
"starting",
|
|
20
|
+
"backing_up",
|
|
16
21
|
"pulling",
|
|
17
22
|
"installing",
|
|
18
23
|
"building",
|
|
19
24
|
"restarting",
|
|
25
|
+
"verifying",
|
|
26
|
+
"rolling_back",
|
|
20
27
|
];
|
|
21
28
|
|
|
29
|
+
/** How a finished run ended; written by scripts/update-run.mjs. */
|
|
30
|
+
export type UpdateOutcome =
|
|
31
|
+
"done" | "build_failed" | "rolled_back" | "broken" | "interrupted";
|
|
32
|
+
|
|
22
33
|
export const isActivePhase = (p: UpdatePhase | undefined | null) =>
|
|
23
34
|
!!p && ACTIVE_PHASES.includes(p);
|
|
24
35
|
|
|
@@ -32,6 +43,44 @@ export interface UpdateRun {
|
|
|
32
43
|
finishedAt?: number;
|
|
33
44
|
error?: string;
|
|
34
45
|
log?: string[];
|
|
46
|
+
/** Started by automatic updates, not by someone. */
|
|
47
|
+
auto?: boolean;
|
|
48
|
+
/** The commit it updates to. */
|
|
49
|
+
to?: string;
|
|
50
|
+
outcome?: UpdateOutcome;
|
|
51
|
+
/** How far redeploy got: pulled, deps_swapped, build_swapped. */
|
|
52
|
+
stage?: string;
|
|
53
|
+
/** The runner's heartbeat and pid, so a cut-off run is told from a slow one. */
|
|
54
|
+
updatedAt?: number;
|
|
55
|
+
pid?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One finished update, kept in the history Settings shows. */
|
|
59
|
+
export interface UpdateAttempt {
|
|
60
|
+
id: string;
|
|
61
|
+
from: string;
|
|
62
|
+
to: string;
|
|
63
|
+
fromVersion: string;
|
|
64
|
+
toVersion: string;
|
|
65
|
+
auto: boolean;
|
|
66
|
+
startedAt: number;
|
|
67
|
+
finishedAt: number;
|
|
68
|
+
outcome: UpdateOutcome;
|
|
69
|
+
error?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The idle times Settings offers for automatic updates. */
|
|
73
|
+
export const IDLE_MINUTES = [5, 10, 15, 30, 60] as const;
|
|
74
|
+
|
|
75
|
+
/** What automatic updates are doing, for Settings. */
|
|
76
|
+
export interface AutoUpdateView {
|
|
77
|
+
enabled: boolean;
|
|
78
|
+
locked: boolean;
|
|
79
|
+
managed: boolean;
|
|
80
|
+
idleMinutes: number;
|
|
81
|
+
idleLocked: boolean;
|
|
82
|
+
status: { code: string; text: string } | null;
|
|
83
|
+
attempts: UpdateAttempt[];
|
|
35
84
|
}
|
|
36
85
|
|
|
37
86
|
export type UpdateRefusal =
|
|
@@ -45,6 +94,7 @@ export type UpdateRefusal =
|
|
|
45
94
|
| "up_to_date"
|
|
46
95
|
| "unknown"
|
|
47
96
|
| "no_restart"
|
|
97
|
+
| "disk"
|
|
48
98
|
| "forbidden";
|
|
49
99
|
|
|
50
100
|
export interface UpdateView {
|
|
@@ -68,10 +118,18 @@ export interface UpdateView {
|
|
|
68
118
|
run: UpdateRun | null;
|
|
69
119
|
pending: { requestedAt: number } | null;
|
|
70
120
|
working: { sessions: number; reviews: number; setups: number };
|
|
121
|
+
auto?: AutoUpdateView;
|
|
71
122
|
}
|
|
72
123
|
|
|
73
124
|
export type BannerState =
|
|
74
|
-
|
|
125
|
+
| "hidden"
|
|
126
|
+
| "available"
|
|
127
|
+
| "pending"
|
|
128
|
+
| "running"
|
|
129
|
+
| "failed"
|
|
130
|
+
| "built"
|
|
131
|
+
| "updated"
|
|
132
|
+
| "rolled_back";
|
|
75
133
|
|
|
76
134
|
export const DISMISSED_VERSION_KEY = "agentos.update.dismissed";
|
|
77
135
|
export const DISMISSED_RUN_KEY = "agentos.update.dismissedRun";
|
|
@@ -79,23 +137,40 @@ export const DISMISSED_RUN_KEY = "agentos.update.dismissedRun";
|
|
|
79
137
|
/**
|
|
80
138
|
* A result of something the person started (running, failed, built) always
|
|
81
139
|
* shows until its run is dismissed; "available" is the one that can be
|
|
82
|
-
* switched off or dismissed, and only for the version dismissed.
|
|
140
|
+
* switched off or dismissed, and only for the version dismissed. An
|
|
141
|
+
* automatic update says it happened ("updated") or that it was rolled back;
|
|
142
|
+
* an automatic one that failed to build changed nothing, so only Settings
|
|
143
|
+
* shows it, unless it left the server broken.
|
|
83
144
|
*/
|
|
145
|
+
export const UPDATED_NOTE_MS = 3 * 24 * 60 * 60_000;
|
|
146
|
+
|
|
84
147
|
export function bannerState({
|
|
85
148
|
view,
|
|
86
149
|
dismissedVersion,
|
|
87
150
|
dismissedRun,
|
|
151
|
+
now = Date.now(),
|
|
88
152
|
}: {
|
|
89
153
|
view: UpdateView | null | undefined;
|
|
90
154
|
dismissedVersion: string | null;
|
|
91
155
|
dismissedRun: string | null;
|
|
156
|
+
now?: number;
|
|
92
157
|
}): BannerState {
|
|
93
158
|
if (!view) return "hidden";
|
|
94
159
|
const run = view.run;
|
|
95
160
|
if (isActivePhase(run?.phase)) return "running";
|
|
96
161
|
if (run && dismissedRun !== run.id) {
|
|
97
|
-
if (run.phase === "
|
|
162
|
+
if (run.phase === "rolled_back") return "rolled_back";
|
|
163
|
+
if (run.phase === "failed" && (!run.auto || run.outcome === "broken"))
|
|
164
|
+
return "failed";
|
|
98
165
|
if (run.phase === "built") return "built";
|
|
166
|
+
// A device that wasn't open for days doesn't need the news.
|
|
167
|
+
if (
|
|
168
|
+
run.phase === "done" &&
|
|
169
|
+
run.auto &&
|
|
170
|
+
view.bannerEnabled &&
|
|
171
|
+
now - (run.finishedAt ?? 0) < UPDATED_NOTE_MS
|
|
172
|
+
)
|
|
173
|
+
return "updated";
|
|
99
174
|
}
|
|
100
175
|
if (view.pending) return "pending";
|
|
101
176
|
if (
|
package/lib/updates/check.ts
CHANGED
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
// or mirror works too. The working tree is never touched.
|
|
4
4
|
|
|
5
5
|
import { git, GIT_TIMEOUT_MS } from "@/lib/git-exec";
|
|
6
|
-
import {
|
|
6
|
+
import type { Candidate } from "./auto";
|
|
7
|
+
import { autoEnabled, bannerEnabled, getSetting, putSetting } from "./settings";
|
|
7
8
|
import { isNewer, versionFor } from "./version";
|
|
8
9
|
|
|
9
10
|
export const CHECK_EVERY_MS = 6 * 60 * 60_000;
|
|
11
|
+
/** With automatic updates on, a release is seen within about an hour. */
|
|
12
|
+
export const AUTO_CHECK_EVERY_MS = 60 * 60_000;
|
|
13
|
+
const CANDIDATES_MAX = 50;
|
|
10
14
|
export const FORCE_FLOOR_MS = 60_000;
|
|
11
15
|
const FIRST_CHECK_MS = 2 * 60_000;
|
|
12
16
|
const NOTES_MAX = 8;
|
|
@@ -26,6 +30,8 @@ export interface CheckResult {
|
|
|
26
30
|
branch: string | null;
|
|
27
31
|
checkedAt: number;
|
|
28
32
|
error: string | null;
|
|
33
|
+
/** First-parent commits between HEAD and the remote, newest first. */
|
|
34
|
+
candidates?: Candidate[];
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
export interface CheckDeps {
|
|
@@ -74,7 +80,7 @@ const NO_REPO: Omit<CheckResult, "checkedAt"> = {
|
|
|
74
80
|
error: null,
|
|
75
81
|
};
|
|
76
82
|
|
|
77
|
-
async function versionAt(
|
|
83
|
+
export async function versionAt(
|
|
78
84
|
g: CheckDeps["git"],
|
|
79
85
|
ref: string
|
|
80
86
|
): Promise<{ version: string; sha: string }> {
|
|
@@ -145,6 +151,22 @@ export async function compare(deps: CheckDeps): Promise<CheckResult> {
|
|
|
145
151
|
.map((s) => s.trim())
|
|
146
152
|
.filter(Boolean)
|
|
147
153
|
: [];
|
|
154
|
+
const candidates =
|
|
155
|
+
behind > 0 && fastForward
|
|
156
|
+
? (
|
|
157
|
+
await g([
|
|
158
|
+
"log",
|
|
159
|
+
"--first-parent",
|
|
160
|
+
"--format=%H %ct",
|
|
161
|
+
`-n${CANDIDATES_MAX}`,
|
|
162
|
+
`HEAD..${remoteRef}`,
|
|
163
|
+
])
|
|
164
|
+
)
|
|
165
|
+
.split("\n")
|
|
166
|
+
.map((l) => l.trim().split(" "))
|
|
167
|
+
.filter(([sha, at]) => /^[0-9a-f]{40}$/.test(sha) && Number(at) > 0)
|
|
168
|
+
.map(([sha, at]) => ({ sha, at: Number(at) * 1000 }))
|
|
169
|
+
: [];
|
|
148
170
|
const status = behind > 0 && !fastForward ? "diverged" : "ok";
|
|
149
171
|
return {
|
|
150
172
|
status,
|
|
@@ -160,6 +182,7 @@ export async function compare(deps: CheckDeps): Promise<CheckResult> {
|
|
|
160
182
|
branch,
|
|
161
183
|
checkedAt,
|
|
162
184
|
error: null,
|
|
185
|
+
candidates,
|
|
163
186
|
};
|
|
164
187
|
}
|
|
165
188
|
|
|
@@ -188,13 +211,36 @@ export async function reconcile(
|
|
|
188
211
|
available: false,
|
|
189
212
|
behind: 0,
|
|
190
213
|
current: { version: check.latest.version, sha: head },
|
|
214
|
+
candidates: [],
|
|
191
215
|
}
|
|
192
|
-
:
|
|
216
|
+
: {
|
|
217
|
+
...check,
|
|
218
|
+
// Commits the checkout already has aren't updates any more.
|
|
219
|
+
candidates: await keepNewer(check.candidates, deps),
|
|
220
|
+
};
|
|
193
221
|
} catch {
|
|
194
222
|
return check;
|
|
195
223
|
}
|
|
196
224
|
}
|
|
197
225
|
|
|
226
|
+
async function keepNewer(
|
|
227
|
+
candidates: Candidate[] | undefined,
|
|
228
|
+
deps: Pick<CheckDeps, "git">
|
|
229
|
+
): Promise<Candidate[]> {
|
|
230
|
+
const out: Candidate[] = [];
|
|
231
|
+
for (const c of candidates ?? []) {
|
|
232
|
+
const has = await deps
|
|
233
|
+
.git(["merge-base", "--is-ancestor", c.sha, "HEAD"])
|
|
234
|
+
.then(
|
|
235
|
+
() => true,
|
|
236
|
+
() => false
|
|
237
|
+
);
|
|
238
|
+
if (has) break;
|
|
239
|
+
out.push(c);
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
|
|
198
244
|
let memo: CheckResult | null = null;
|
|
199
245
|
let inflight: Promise<CheckResult> | null = null;
|
|
200
246
|
|
|
@@ -214,7 +260,11 @@ export function cachedCheck(deps: CheckDeps = realDeps()): CheckResult | null {
|
|
|
214
260
|
* fetch keeps the last good answer and notes the error.
|
|
215
261
|
*/
|
|
216
262
|
export async function checkForUpdate(
|
|
217
|
-
{
|
|
263
|
+
{
|
|
264
|
+
force = false,
|
|
265
|
+
always = false,
|
|
266
|
+
maxAge = CHECK_EVERY_MS,
|
|
267
|
+
}: { force?: boolean; always?: boolean; maxAge?: number } = {},
|
|
218
268
|
deps: CheckDeps = realDeps()
|
|
219
269
|
): Promise<{ result: CheckResult; throttled?: number }> {
|
|
220
270
|
const last = cachedCheck(deps);
|
|
@@ -224,7 +274,7 @@ export async function checkForUpdate(
|
|
|
224
274
|
result: last,
|
|
225
275
|
throttled: Math.ceil((FORCE_FLOOR_MS - age) / 1000),
|
|
226
276
|
};
|
|
227
|
-
if (last && !force && age <
|
|
277
|
+
if (last && !force && age < maxAge) return { result: last };
|
|
228
278
|
inflight ??= compare(deps)
|
|
229
279
|
.catch((error: unknown): CheckResult => {
|
|
230
280
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -254,17 +304,23 @@ export function updateChecksEnabled(
|
|
|
254
304
|
|
|
255
305
|
const g = globalThis as unknown as { __agentosUpdateChecks?: NodeJS.Timeout };
|
|
256
306
|
|
|
257
|
-
/**
|
|
307
|
+
/**
|
|
308
|
+
* First check two minutes after boot, then every six hours, or every hour
|
|
309
|
+
* with automatic updates on (jittered). Off when both the banner and
|
|
310
|
+
* automatic updates are.
|
|
311
|
+
*/
|
|
258
312
|
export function startUpdateChecks(): void {
|
|
259
313
|
if (g.__agentosUpdateChecks) return;
|
|
260
314
|
const schedule = (delay: number): void => {
|
|
261
315
|
g.__agentosUpdateChecks = setTimeout(async () => {
|
|
316
|
+
const every = autoEnabled() ? AUTO_CHECK_EVERY_MS : CHECK_EVERY_MS;
|
|
262
317
|
try {
|
|
263
|
-
if (bannerEnabled()
|
|
318
|
+
if (bannerEnabled() || autoEnabled())
|
|
319
|
+
await checkForUpdate({ maxAge: every - 5 * 60_000 });
|
|
264
320
|
} catch (error) {
|
|
265
321
|
console.error("Checking for updates failed:", error);
|
|
266
322
|
}
|
|
267
|
-
schedule(
|
|
323
|
+
schedule(every + Math.floor(Math.random() * 10 * 60_000));
|
|
268
324
|
}, delay);
|
|
269
325
|
g.__agentosUpdateChecks.unref();
|
|
270
326
|
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// The finished updates, newest first, and what they say about trying a
|
|
2
|
+
// version again. Pure apart from the two setting accessors passed in.
|
|
3
|
+
|
|
4
|
+
import type { UpdateAttempt, UpdateRun } from "./banner";
|
|
5
|
+
|
|
6
|
+
export const HISTORY_KEY = "updates.attempts";
|
|
7
|
+
const KEEP = 20;
|
|
8
|
+
/** A version that failed to build is tried again after these waits, then given up on. */
|
|
9
|
+
export const RETRY_AFTER_MS = [60 * 60_000, 6 * 60 * 60_000] as const;
|
|
10
|
+
|
|
11
|
+
type Get = (key: string) => string | null;
|
|
12
|
+
type Put = (key: string, value: string | null) => void;
|
|
13
|
+
|
|
14
|
+
export function readAttempts(get: Get): UpdateAttempt[] {
|
|
15
|
+
try {
|
|
16
|
+
const list = JSON.parse(get(HISTORY_KEY) ?? "[]") as UpdateAttempt[];
|
|
17
|
+
return Array.isArray(list) ? list : [];
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The attempt a finished run makes, or null while it runs or when it isn't one. */
|
|
24
|
+
export function attemptOf(run: UpdateRun | null): UpdateAttempt | null {
|
|
25
|
+
if (!run?.outcome || !run.finishedAt || !run.to) return null;
|
|
26
|
+
return {
|
|
27
|
+
id: run.id,
|
|
28
|
+
from: run.from,
|
|
29
|
+
to: run.to,
|
|
30
|
+
fromVersion: run.fromVersion,
|
|
31
|
+
toVersion: run.toVersion,
|
|
32
|
+
auto: !!run.auto,
|
|
33
|
+
startedAt: run.startedAt,
|
|
34
|
+
finishedAt: run.finishedAt,
|
|
35
|
+
outcome: run.outcome,
|
|
36
|
+
...(run.error ? { error: run.error } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Adds an attempt once (by id), newest first; true when it was new. */
|
|
41
|
+
export function recordAttempt(
|
|
42
|
+
get: Get,
|
|
43
|
+
put: Put,
|
|
44
|
+
attempt: UpdateAttempt
|
|
45
|
+
): boolean {
|
|
46
|
+
const list = readAttempts(get);
|
|
47
|
+
if (list.some((a) => a.id === attempt.id)) return false;
|
|
48
|
+
put(HISTORY_KEY, JSON.stringify([attempt, ...list].slice(0, KEEP)));
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type Block =
|
|
53
|
+
| { kind: "bad"; reason: string }
|
|
54
|
+
| { kind: "later"; until: number; reason: string };
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Why a commit shouldn't be tried now: it was rolled back or broke the
|
|
58
|
+
* server (never again), failed to build too often (never again), or failed
|
|
59
|
+
* recently (after a wait). Interrupted runs say nothing about the commit.
|
|
60
|
+
*/
|
|
61
|
+
export function blockFor(
|
|
62
|
+
sha: string,
|
|
63
|
+
attempts: UpdateAttempt[],
|
|
64
|
+
now: number
|
|
65
|
+
): Block | null {
|
|
66
|
+
const mine = attempts.filter((a) => a.to === sha);
|
|
67
|
+
if (mine.some((a) => a.outcome === "rolled_back" || a.outcome === "broken"))
|
|
68
|
+
return {
|
|
69
|
+
kind: "bad",
|
|
70
|
+
reason: "it didn't start cleanly last time, so it won't be tried again",
|
|
71
|
+
};
|
|
72
|
+
// Only failures since it last worked count: a version can be updated to,
|
|
73
|
+
// rolled back by hand, and updated to again.
|
|
74
|
+
const fails: UpdateAttempt[] = [];
|
|
75
|
+
for (const a of mine) {
|
|
76
|
+
if (a.outcome === "done") break;
|
|
77
|
+
if (a.outcome === "build_failed") fails.push(a);
|
|
78
|
+
}
|
|
79
|
+
if (!fails.length) return null;
|
|
80
|
+
if (fails.length > RETRY_AFTER_MS.length)
|
|
81
|
+
return {
|
|
82
|
+
kind: "bad",
|
|
83
|
+
reason: `it failed to build ${fails.length} times, so it won't be tried again`,
|
|
84
|
+
};
|
|
85
|
+
const until = fails[0].finishedAt + RETRY_AFTER_MS[fails.length - 1];
|
|
86
|
+
return until > now
|
|
87
|
+
? { kind: "later", until, reason: "it failed to build last time" }
|
|
88
|
+
: null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Failed automatic attempts since the last update that worked (by anyone). */
|
|
92
|
+
export function failedInARow(attempts: UpdateAttempt[]): number {
|
|
93
|
+
let n = 0;
|
|
94
|
+
for (const a of attempts) {
|
|
95
|
+
if (a.outcome === "done") break;
|
|
96
|
+
if (a.auto && a.outcome !== "interrupted") n++;
|
|
97
|
+
}
|
|
98
|
+
return n;
|
|
99
|
+
}
|