getbotons 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +505 -2
- package/installer/admin.mjs +6 -0
- package/installer/botons.mjs +430 -0
- package/installer/brain-clis.mjs +28 -0
- package/installer/linger.mjs +39 -0
- package/installer/output.mjs +27 -0
- package/installer/revision.mjs +30 -0
- package/installer/shell-path.mjs +66 -0
- package/installer/source-lock.json +2891 -0
- package/package.json +26 -4
- package/payload/LICENSE +21 -0
- package/payload/SECURITY.md +33 -0
- package/payload/installer/admin.mjs +6 -0
- package/payload/installer/botons.mjs +430 -0
- package/payload/installer/brain-clis.mjs +28 -0
- package/payload/installer/linger.mjs +39 -0
- package/payload/installer/output.mjs +27 -0
- package/payload/installer/revision.mjs +30 -0
- package/payload/installer/shell-path.mjs +66 -0
- package/payload/installer/source-lock.json +2891 -0
- package/payload/npm-shrinkwrap.json +51 -0
- package/payload/package.json +13 -0
- package/payload/scripts/backup.ts +47 -0
- package/payload/scripts/botons-backup.service +15 -0
- package/payload/scripts/botons-backup.timer +11 -0
- package/payload/scripts/botons.service +36 -0
- package/payload/scripts/check-browser.ts +90 -0
- package/payload/scripts/doctor.ts +790 -0
- package/payload/scripts/install-backup-timer.ts +52 -0
- package/payload/scripts/install-service.ts +206 -0
- package/payload/scripts/login.ts +17 -0
- package/payload/scripts/migrate-names.ts +47 -0
- package/payload/scripts/pair.ts +163 -0
- package/payload/scripts/restore.ts +108 -0
- package/payload/server/bin/botons +4 -0
- package/payload/server/bin/legacy/minion +4 -0
- package/payload/server/bin/legacy/tots +4 -0
- package/payload/server/src/acting.ts +16 -0
- package/payload/server/src/api-keys.ts +544 -0
- package/payload/server/src/api.ts +1484 -0
- package/payload/server/src/app.ts +546 -0
- package/payload/server/src/approvals.ts +873 -0
- package/payload/server/src/ask-rules.ts +181 -0
- package/payload/server/src/audit.ts +158 -0
- package/payload/server/src/auth.ts +497 -0
- package/payload/server/src/backup.ts +382 -0
- package/payload/server/src/brain-login.ts +75 -0
- package/payload/server/src/browser-cert.ts +67 -0
- package/payload/server/src/browser-handoff.ts +119 -0
- package/payload/server/src/browser-handover.ts +175 -0
- package/payload/server/src/browser-policy.ts +210 -0
- package/payload/server/src/browser-proxy.ts +580 -0
- package/payload/server/src/browser-request.ts +335 -0
- package/payload/server/src/browser-screenshot.ts +56 -0
- package/payload/server/src/browser-send.ts +155 -0
- package/payload/server/src/browser.ts +2164 -0
- package/payload/server/src/callback.ts +218 -0
- package/payload/server/src/capabilities.ts +90 -0
- package/payload/server/src/cards.ts +243 -0
- package/payload/server/src/cli/approve-mcp.ts +119 -0
- package/payload/server/src/cli/botons.ts +629 -0
- package/payload/server/src/cli/browser-mcp.ts +168 -0
- package/payload/server/src/cli/workspace-file.ts +78 -0
- package/payload/server/src/cli-homes.ts +38 -0
- package/payload/server/src/cli-install.ts +104 -0
- package/payload/server/src/cli-updates.ts +112 -0
- package/payload/server/src/codex-config-repair.ts +80 -0
- package/payload/server/src/config.ts +473 -0
- package/payload/server/src/connector-access.ts +158 -0
- package/payload/server/src/connector-launch.ts +203 -0
- package/payload/server/src/connectors.ts +947 -0
- package/payload/server/src/disk-usage.ts +62 -0
- package/payload/server/src/engine.ts +2328 -0
- package/payload/server/src/env-init.ts +5 -0
- package/payload/server/src/events.ts +46 -0
- package/payload/server/src/files.ts +528 -0
- package/payload/server/src/github-access.ts +52 -0
- package/payload/server/src/health.ts +117 -0
- package/payload/server/src/held-dir.ts +96 -0
- package/payload/server/src/hooks.ts +141 -0
- package/payload/server/src/http.ts +207 -0
- package/payload/server/src/index.ts +139 -0
- package/payload/server/src/install-type.ts +11 -0
- package/payload/server/src/instructions.ts +204 -0
- package/payload/server/src/isolation.ts +234 -0
- package/payload/server/src/legacy-upgrade.ts +668 -0
- package/payload/server/src/limits.ts +44 -0
- package/payload/server/src/lock.ts +75 -0
- package/payload/server/src/log.ts +137 -0
- package/payload/server/src/managed-clis.ts +65 -0
- package/payload/server/src/mcp-client.ts +229 -0
- package/payload/server/src/notify.ts +193 -0
- package/payload/server/src/oauth-connections.ts +650 -0
- package/payload/server/src/oauth.ts +311 -0
- package/payload/server/src/outbound.ts +142 -0
- package/payload/server/src/portable.ts +1669 -0
- package/payload/server/src/prompt.ts +676 -0
- package/payload/server/src/providers/api-brain.ts +612 -0
- package/payload/server/src/providers/api-history.ts +131 -0
- package/payload/server/src/providers/api-tools.ts +481 -0
- package/payload/server/src/providers/api-wire.ts +690 -0
- package/payload/server/src/providers/claude-cli.ts +620 -0
- package/payload/server/src/providers/cli-check.ts +265 -0
- package/payload/server/src/providers/cli-turn.ts +20 -0
- package/payload/server/src/providers/codex-cli.ts +552 -0
- package/payload/server/src/providers/codex-workspace.ts +61 -0
- package/payload/server/src/providers/failures.ts +143 -0
- package/payload/server/src/providers/index.ts +131 -0
- package/payload/server/src/providers/mcp-stdio.ts +147 -0
- package/payload/server/src/providers/mock.ts +94 -0
- package/payload/server/src/providers/process.ts +268 -0
- package/payload/server/src/providers/readiness.ts +43 -0
- package/payload/server/src/providers/sse.ts +73 -0
- package/payload/server/src/providers/types.ts +173 -0
- package/payload/server/src/rates.ts +128 -0
- package/payload/server/src/reactions.ts +266 -0
- package/payload/server/src/recent-logs.ts +489 -0
- package/payload/server/src/rehearsal.ts +593 -0
- package/payload/server/src/rehearsals.ts +303 -0
- package/payload/server/src/replay.ts +165 -0
- package/payload/server/src/sandbox.ts +767 -0
- package/payload/server/src/schedule.ts +214 -0
- package/payload/server/src/scheduler.ts +24 -0
- package/payload/server/src/schema.ts +882 -0
- package/payload/server/src/secrets.ts +46 -0
- package/payload/server/src/setup.ts +356 -0
- package/payload/server/src/skill-packages.ts +363 -0
- package/payload/server/src/skills.ts +527 -0
- package/payload/server/src/spend.ts +255 -0
- package/payload/server/src/ssrf.ts +126 -0
- package/payload/server/src/starter-gate.ts +168 -0
- package/payload/server/src/starters.ts +610 -0
- package/payload/server/src/storage.ts +32 -0
- package/payload/server/src/store.ts +3087 -0
- package/payload/server/src/tailnet.ts +132 -0
- package/payload/server/src/templates.ts +454 -0
- package/payload/server/src/trash.ts +772 -0
- package/payload/server/src/util.ts +97 -0
- package/payload/server/src/webhooks.ts +216 -0
- package/payload/server/src/work-feed.ts +473 -0
- package/payload/shared/app-version.d.mts +1 -0
- package/payload/shared/app-version.mjs +3 -0
- package/payload/shared/boton.ts +323 -0
- package/payload/shared/brand.ts +107 -0
- package/payload/shared/cards.ts +427 -0
- package/payload/shared/effort.ts +91 -0
- package/payload/shared/files.ts +104 -0
- package/payload/shared/handoff.ts +97 -0
- package/payload/shared/install-commands.ts +7 -0
- package/payload/shared/legacy-names.d.mts +35 -0
- package/payload/shared/legacy-names.mjs +244 -0
- package/payload/shared/live-view.ts +94 -0
- package/payload/shared/look-critter.ts +189 -0
- package/payload/shared/look-tater.ts +127 -0
- package/payload/shared/look.ts +200 -0
- package/payload/shared/reactions.ts +43 -0
- package/payload/shared/seeded.ts +34 -0
- package/payload/shared/services.ts +90 -0
- package/payload/shared/types.ts +3016 -0
- package/payload/shared/util.ts +68 -0
- package/payload/shared/versions.ts +44 -0
- package/payload/shared/wake.ts +164 -0
- package/payload/web/dist/apple-touch-icon.png +0 -0
- package/payload/web/dist/assets/Icons-nytVAa9g.js +1 -0
- package/payload/web/dist/assets/NewBotDialog-DXr2XhDP.js +1 -0
- package/payload/web/dist/assets/PairScreen-CMcJO7cQ.js +1 -0
- package/payload/web/dist/assets/Panel-Bvqz3I3m.js +4 -0
- package/payload/web/dist/assets/ReactionPicker-B0SKsjiy.js +1 -0
- package/payload/web/dist/assets/SettingRows-CvLomtuo.js +4 -0
- package/payload/web/dist/assets/SettingsDialog-CQ41LV-P.js +8 -0
- package/payload/web/dist/assets/Setup-C-3P7Mtn.js +1 -0
- package/payload/web/dist/assets/Skills-DXMH69kH.js +1 -0
- package/payload/web/dist/assets/StarterSetup-Ce2V1_Cs.js +5 -0
- package/payload/web/dist/assets/Steps-CKtB3GAM.js +1 -0
- package/payload/web/dist/assets/emoji-catalog-DfpPz8qw.js +540 -0
- package/payload/web/dist/assets/index-BEU7FDxV.css +1 -0
- package/payload/web/dist/assets/index-Dhwn-rf0.js +3 -0
- package/payload/web/dist/assets/react-C1iE8mnY.js +9 -0
- package/payload/web/dist/assets/reactions-rw0YeCmA.js +1 -0
- package/payload/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/payload/web/dist/assets/util-Cx5feSJu.js +1 -0
- package/payload/web/dist/assets/vendor-B3qdcjGo.js +14 -0
- package/payload/web/dist/favicon.svg +1 -0
- package/payload/web/dist/icon-192.png +0 -0
- package/payload/web/dist/icon-512.png +0 -0
- package/payload/web/dist/icon.svg +1 -0
- package/payload/web/dist/index.html +67 -0
- package/payload/web/dist/manifest.webmanifest +12 -0
- package/shared/legacy-names.d.mts +35 -0
- package/shared/legacy-names.mjs +244 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { types } from 'node:util';
|
|
2
|
+
import { EFFORT_IDS } from '../../shared/effort.ts';
|
|
3
|
+
import type { RecentLogEntry, RecentLogField, RecentLogHelp, RecentLogLevel, RecentLogs as RecentLogsAnswer } from '../../shared/types.ts';
|
|
4
|
+
import { type LogFields, type LogLevel, tapLogs } from './log.ts';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Settings → Server events: a selection of what this process logged, kept in memory only (a restart starts it
|
|
8
|
+
* again; nothing reaches the disk or a backup). It is not the log. Nothing a line carries is copied as text:
|
|
9
|
+
*
|
|
10
|
+
* - The message is looked up, whole and exactly, in EVENTS below, a catalog of the fixed messages the server
|
|
11
|
+
* writes. A known message becomes its event code and label (both written here, in the source); any other
|
|
12
|
+
* message, including every message built from values, becomes "unclassified" with a fixed label.
|
|
13
|
+
* - Only the field names in FIELDS are read, each through its own data property (never a getter, proxy trap,
|
|
14
|
+
* toJSON, toString or prototype), and a value is kept only when its validator accepts it: ids of the server's
|
|
15
|
+
* own shape, enum words, booleans and bounded whole numbers. Anything else is left out, never transformed.
|
|
16
|
+
* - A thrown value (`err`) becomes a fixed error category from its own `code` (or its cause's), never its text.
|
|
17
|
+
*
|
|
18
|
+
* Every check reads a fixed number of properties and looks at no more than a short string's length first, so a
|
|
19
|
+
* line costs the same whatever it holds. The full log, with error text, stays where the service sends stdout.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Newest lines kept; the oldest go first. */
|
|
23
|
+
export const RECENT_LOG_ENTRIES = 1000;
|
|
24
|
+
/** And at most this many UTF-8 bytes (the entries' JSON), whichever limit comes first. */
|
|
25
|
+
export const RECENT_LOG_BYTES = 512 * 1024;
|
|
26
|
+
/** Lines one answer holds at most. */
|
|
27
|
+
export const RECENT_LOG_PAGE = 200;
|
|
28
|
+
|
|
29
|
+
export const UNCLASSIFIED = { event: 'unclassified', label: 'Unclassified server event' } as const;
|
|
30
|
+
|
|
31
|
+
const LEVELS: readonly RecentLogLevel[] = ['info', 'warn', 'error'];
|
|
32
|
+
/** Longer than any message in EVENTS: a longer message is unclassified without being looked at. */
|
|
33
|
+
const MESSAGE_MAX = 200;
|
|
34
|
+
const QUERY_MAX = 100;
|
|
35
|
+
|
|
36
|
+
type EventDef = readonly [message: string, event: string, help?: RecentLogHelp, label?: string];
|
|
37
|
+
|
|
38
|
+
/** The server's fixed log messages (server/test/recent-logs.test.ts keeps this in step with server/src). */
|
|
39
|
+
const EVENTS: readonly EventDef[] = [
|
|
40
|
+
["routine models reset: the Boton moved to another provider", 'routine.models_reset'],
|
|
41
|
+
["codex turn's own tokens are unknown: turn.completed has the thread's running totals, and its rollout doesn't say where this turn started", 'codex.usage_unknown'],
|
|
42
|
+
['a setting uses a deprecated name, which works for this release only', 'config.earlier_name'],
|
|
43
|
+
['a setting is set under both its name and a deprecated name, to different values: its name is used', 'config.earlier_name_conflict'],
|
|
44
|
+
['did not put back an instruction file a sandboxed turn changed: a folder on its way is now a link, which is not followed', 'sandbox.restore_link_refused'],
|
|
45
|
+
['a connector test failed', 'connector.test_failed', 'connector'],
|
|
46
|
+
['a connector was turned off or on during a poll of its work feed; the rest of that poll was dropped', 'connector.feed_poll_dropped'],
|
|
47
|
+
['agent CLIs left by a previous server process would not stop', 'agents.orphans_kept', 'bug'],
|
|
48
|
+
['API key removed', 'api_key.removed'],
|
|
49
|
+
['API key saved', 'api_key.saved'],
|
|
50
|
+
['API key tested', 'api_key.tested'],
|
|
51
|
+
['api turn', 'api.turn'],
|
|
52
|
+
['approval decided', 'approval.decided'],
|
|
53
|
+
['approval request allowed by an always-allow rule', 'approval.always_allowed'],
|
|
54
|
+
['approval requested', 'approval.requested'],
|
|
55
|
+
['approval request expired', 'approval.expired'],
|
|
56
|
+
['approval request refused: rate limit', 'approval.rate_limited'],
|
|
57
|
+
['approval request withdrawn', 'approval.withdrawn'],
|
|
58
|
+
['a sandboxed turn changed instruction files; put them back', 'sandbox.instructions_restored'],
|
|
59
|
+
['the Workspace sandbox for API-key Botons is unavailable', 'sandbox.unavailable'],
|
|
60
|
+
['the Workspace sandbox leaves out folders another filesystem is mounted inside', 'sandbox.mount_parents_omitted'],
|
|
61
|
+
['could not save an API session', 'api.session_save_failed'],
|
|
62
|
+
['spending caps changed', 'spending.caps_changed'],
|
|
63
|
+
['API requests a stopped server left reserved are held at their most', 'spending.reservations_held'],
|
|
64
|
+
['ask answered', 'ask.answered'],
|
|
65
|
+
['ask refused: hop limit', 'ask.hop_limited'],
|
|
66
|
+
['ask refused: rate limit', 'ask.rate_limited'],
|
|
67
|
+
['ask sent', 'ask.sent'],
|
|
68
|
+
['auth is OFF (BOTONS_AUTH=off): anyone who can reach this server can use it, Botons included.', 'auth.off', 'auth-off'],
|
|
69
|
+
['backed up the database before upgrading its schema', 'schema.backed_up'],
|
|
70
|
+
['browser closed', 'browser.closed'],
|
|
71
|
+
['browser handoff handed back', 'browser.handoff_handed_back'],
|
|
72
|
+
['browser handoff requested', 'browser.handoff_requested'],
|
|
73
|
+
['browser handover ended', 'browser.handover_ended'],
|
|
74
|
+
['browser handover started', 'browser.handover_started'],
|
|
75
|
+
['browser request held for approval', 'browser.request_held'],
|
|
76
|
+
['browser request sent', 'browser.request_sent'],
|
|
77
|
+
['browser request withdrawn', 'browser.request_withdrawn'],
|
|
78
|
+
['browser sites changed', 'browser.sites_changed'],
|
|
79
|
+
['browser started', 'browser.started'],
|
|
80
|
+
['callback failed', 'callback.failed', 'run'],
|
|
81
|
+
['callback not sent: too many results waiting for the callback URL', 'callback.backlog', 'run'],
|
|
82
|
+
['callback resumed after a restart', 'callback.resumed'],
|
|
83
|
+
['callback sent', 'callback.sent'],
|
|
84
|
+
['card answered', 'card.answered'],
|
|
85
|
+
['card posted', 'card.posted'],
|
|
86
|
+
['connector granted at once on request', 'connector.granted'],
|
|
87
|
+
['connector requested: waiting for approval', 'connector.requested'],
|
|
88
|
+
['connector request policy changed', 'connector.policy_changed'],
|
|
89
|
+
["connector's Botons changed", 'connector.botons_changed'],
|
|
90
|
+
['connectors left out of the turn', 'turn.connectors_left_out'],
|
|
91
|
+
['connectors left out of the turn: a webhook started it', 'turn.connectors_left_out_webhook'],
|
|
92
|
+
['conversation reset: the next turn starts a new session', 'session.reset_by_owner'],
|
|
93
|
+
['closed file descriptors the server was started with, so no sandboxed command gets them', 'sandbox.inherited_descriptors_closed'],
|
|
94
|
+
['could not check OAuth keep-alive', 'oauth.keepalive_check_failed', 'bug'],
|
|
95
|
+
['could not check the instruction files after a sandboxed turn', 'sandbox.check_failed'],
|
|
96
|
+
['could not delete a handover browser profile', 'browser.handover_profile_left'],
|
|
97
|
+
['could not delete a trash item', 'trash.purge_failed'],
|
|
98
|
+
['could not expire an approval request', 'approval.expire_failed', 'bug'],
|
|
99
|
+
['could not expire approval requests', 'approval.expire_all_failed', 'bug'],
|
|
100
|
+
['could not list a connector’s tools', 'connector.tools_failed', 'connector'],
|
|
101
|
+
['could not note what a test run runs on', 'starter.test_not_noted', 'bug'],
|
|
102
|
+
['could not push a browser handoff', 'push.handoff_failed', 'push'],
|
|
103
|
+
['could not push about a connector that needs signing in', 'push.reconnect_failed', 'push'],
|
|
104
|
+
['could not put back an instruction file a sandboxed turn changed', 'sandbox.restore_failed'],
|
|
105
|
+
['could not put back an instruction file a sandboxed turn changed: it was too big to keep a copy of', 'sandbox.restore_too_big'],
|
|
106
|
+
['could not put back the instruction files a sandboxed turn changed: its workspace could not be opened', 'sandbox.restore_unopened'],
|
|
107
|
+
['could not read a connector’s work feed; trying again later', 'connector.feed_failed', 'connector'],
|
|
108
|
+
['could not read a job’s link; its update goes without one', 'work.link_unreadable'],
|
|
109
|
+
['could not read the browser profile\'s cookies', 'browser.cookies_unread'],
|
|
110
|
+
['could not record a callback for a run', 'callback.record_failed', 'bug'],
|
|
111
|
+
['could not record an agent process group', 'agents.record_failed'],
|
|
112
|
+
['could not record a push for a run', 'push.record_run_failed'],
|
|
113
|
+
['could not record a restore rehearsal', 'backup.rehearsal_record_failed', 'bug'],
|
|
114
|
+
['could not record a tool result', 'tool.record_failed'],
|
|
115
|
+
['could not record the push for an approval request', 'push.record_approval_failed'],
|
|
116
|
+
['could not remove a local connector’s launch file', 'connector.launch_file_left'],
|
|
117
|
+
['could not save the handover\'s cookies to the browser profile', 'browser.cookies_unsaved'],
|
|
118
|
+
['could not send the error response', 'http.error_response_failed', 'bug'],
|
|
119
|
+
['could not start looking at a signed-in connector', 'connector.look_failed', 'connector'],
|
|
120
|
+
['could not stop running turns', 'shutdown.stop_failed', 'bug'],
|
|
121
|
+
['could not write to the browser journal', 'browser.journal_failed'],
|
|
122
|
+
['could not write an audit event', 'audit.write_failed', 'bug'],
|
|
123
|
+
['event listener failed', 'events.listener_failed', 'bug'],
|
|
124
|
+
['file uploaded', 'file.uploaded'],
|
|
125
|
+
['gave up on chat messages cut off at every start', 'chat.cut_off_given_up'],
|
|
126
|
+
['job failed', 'jobs.failed', 'bug'],
|
|
127
|
+
['keep-alive refresh failed', 'oauth.keepalive_failed', 'reconnect'],
|
|
128
|
+
['looked at a connector’s tools', 'connector.looked'],
|
|
129
|
+
['MCP server added to the library', 'mcp.added'],
|
|
130
|
+
['MCP server in the library changed', 'mcp.changed'],
|
|
131
|
+
['MCP server left out of a codex turn', 'mcp.left_out'],
|
|
132
|
+
['MCP server removed from the library', 'mcp.removed'],
|
|
133
|
+
['Boton created from a template', 'boton.created_from_template'],
|
|
134
|
+
['Boton duplicated', 'boton.duplicated'],
|
|
135
|
+
['new claude session', 'session.new'],
|
|
136
|
+
['new session: the replay was sized to its budget', 'session.replay'],
|
|
137
|
+
['webhook run: the chat it was given was sized to its budget', 'session.replay'],
|
|
138
|
+
['OAuth callback refused: the authorization response came from another issuer', 'oauth.callback_wrong_issuer', 'reconnect'],
|
|
139
|
+
['OAuth callback refused: the device that started it is no longer paired', 'oauth.callback_unpaired', 'reconnect'],
|
|
140
|
+
['OAuth code exchange failed', 'oauth.exchange_failed', 'reconnect'],
|
|
141
|
+
['OAuth connector added', 'oauth.added'],
|
|
142
|
+
['OAuth connector removed', 'oauth.removed'],
|
|
143
|
+
['OAuth connector signed in', 'oauth.signed_in'],
|
|
144
|
+
['OAuth refresh answer lost: the refresh token is not used again; the connector needs signing in again', 'oauth.refresh_lost', 'reconnect'],
|
|
145
|
+
['OAuth refresh discarded: the connector changed while it ran', 'oauth.refresh_discarded'],
|
|
146
|
+
['OAuth refresh failed; the grant is kept', 'oauth.refresh_failed', 'reconnect'],
|
|
147
|
+
['OAuth refresh interrupted: the refresh token is not used again; the connector needs signing in again', 'oauth.refresh_interrupted', 'reconnect'],
|
|
148
|
+
['OAuth refresh refused: the connector needs signing in again', 'oauth.refresh_refused', 'reconnect'],
|
|
149
|
+
['OAuth sign-in declined at the authorization server', 'oauth.declined', 'reconnect'],
|
|
150
|
+
['OAuth sign-in discarded: the device that started it was unpaired while it finished', 'oauth.sign_in_discarded', 'reconnect'],
|
|
151
|
+
['OAuth sign-in started', 'oauth.sign_in_started'],
|
|
152
|
+
['OAuth token refreshed', 'oauth.refreshed'],
|
|
153
|
+
['provider switched: the next chat turn starts with a handoff note', 'provider.switched'],
|
|
154
|
+
['push failed', 'push.failed', 'push'],
|
|
155
|
+
['reaction added', 'reaction.added'],
|
|
156
|
+
['reaction removed', 'reaction.removed'],
|
|
157
|
+
['reactions imported', 'reactions.imported'],
|
|
158
|
+
['registered Botons with an authorization server', 'oauth.registered'],
|
|
159
|
+
['request failed', 'http.request_failed', 'bug'],
|
|
160
|
+
['restore rehearsal failed', 'backup.rehearsal_failed'],
|
|
161
|
+
['restore rehearsal passed', 'backup.rehearsal_passed'],
|
|
162
|
+
['run cancelled', 'run.cancelled'],
|
|
163
|
+
['run failed on a network error: trying again later', 'run.network_retry'],
|
|
164
|
+
['run queued', 'run.queued'],
|
|
165
|
+
['run stopped at its time limit', 'run.time_limit', 'run'],
|
|
166
|
+
['scheduled routine failed to start', 'run.schedule_failed', 'run'],
|
|
167
|
+
['scheduled run skipped', 'run.skipped'],
|
|
168
|
+
['session lost: starting a new one', 'session.lost'],
|
|
169
|
+
['session reset: the system prompt or provider changed, so the turn starts a new session', 'session.reset_by_change'],
|
|
170
|
+
['shutdown failed', 'shutdown.failed', 'bug'],
|
|
171
|
+
['skill approved', 'skill.approved'],
|
|
172
|
+
['skill changed', 'skill.changed'],
|
|
173
|
+
['skill deleted', 'skill.deleted'],
|
|
174
|
+
['skill saved', 'skill.saved'],
|
|
175
|
+
['skill installed', 'skill.installed'],
|
|
176
|
+
['skill restored', 'skill.restored'],
|
|
177
|
+
['skill imported', 'skill.imported'],
|
|
178
|
+
['skill turned off', 'skill.turned_off'],
|
|
179
|
+
['skill turned on', 'skill.turned_on'],
|
|
180
|
+
["skill's Botons changed", 'skill.botons_changed'],
|
|
181
|
+
['skill waiting for approval', 'skill.waiting'],
|
|
182
|
+
['some agent processes did not exit in time', 'agents.exit_late'],
|
|
183
|
+
['stopped polling a connector’s feed: it needs signing in again', 'connector.feed_stopped', 'reconnect'],
|
|
184
|
+
['stopping an agent CLI that a previous server process left running (it died without draining), before its work runs again', 'agents.orphan_stopped'],
|
|
185
|
+
['switch summary failed', 'switch.summary_failed'],
|
|
186
|
+
['switch summary not saved: stopped once it was written', 'switch.summary_stopped'],
|
|
187
|
+
['switch summary not saved: the move changed while it was written', 'switch.summary_discarded'],
|
|
188
|
+
['switch summary started', 'switch.summary_started'],
|
|
189
|
+
['switch summary written', 'switch.summary_written'],
|
|
190
|
+
['Tailscale check failed', 'tailscale.check_failed', 'tailscale'],
|
|
191
|
+
['template saved', 'template.saved'],
|
|
192
|
+
['configuration exported', 'config.exported'],
|
|
193
|
+
['configuration imported', 'config.imported'],
|
|
194
|
+
['templates imported', 'template.imported'],
|
|
195
|
+
['the list of imported templates waiting to be put in place is unreadable: it is dropped', 'template.outbox_unreadable'],
|
|
196
|
+
['a step after a commit failed', 'server.after_commit_failed'],
|
|
197
|
+
['the browser did not say its user agent: it keeps its headless one', 'browser.user_agent_unknown'],
|
|
198
|
+
['the browser did not close cleanly', 'browser.close_failed'],
|
|
199
|
+
['the browser did not start', 'browser.start_failed'],
|
|
200
|
+
['the instruction files could not be noted before a sandboxed turn, so they will not be put back after it (the CLI sandbox still keeps it from writing them)', 'sandbox.snapshot_failed'],
|
|
201
|
+
['the managed browser did not start', 'browser.check_failed'],
|
|
202
|
+
['the managed browser is not available', 'browser.unavailable'],
|
|
203
|
+
['the authorization server issued no refresh token: the connector needs signing in again when the access token expires', 'oauth.no_refresh_token', 'reconnect'],
|
|
204
|
+
['tool call allowed by an always-allow rule', 'tool.always_allowed'],
|
|
205
|
+
['tool call allowed: the owner allowed it after it stopped waiting', 'tool.allowed_late'],
|
|
206
|
+
['tool call denied: no Ask first rule covers it, and Workspace access does not allow it', 'tool.denied'],
|
|
207
|
+
['tool call paused for approval', 'tool.paused'],
|
|
208
|
+
['tool call waits on its card again', 'tool.waits_again'],
|
|
209
|
+
['turn ended', 'turn.ended'],
|
|
210
|
+
['turn not started: its provider is not on this server', 'turn.no_provider', 'brain'],
|
|
211
|
+
['turn started', 'turn.started'],
|
|
212
|
+
['unhandled promise rejection', 'process.unhandled_rejection', 'bug'],
|
|
213
|
+
['usage limit has reset: resuming its Botons', 'usage.reset'],
|
|
214
|
+
['usage limit hit: holding its turns until the reset', 'usage.limited', 'usage-limit'],
|
|
215
|
+
['wake-up failed', 'wakeup.failed', 'bug'],
|
|
216
|
+
['could not move an inbox file to the trash', 'trash.inbox_move_failed'],
|
|
217
|
+
['a file changed while it was moved to the trash; what was moved is kept in the trash', 'trash.move_changed_kept'],
|
|
218
|
+
['a restored file was not where it was put', 'trash.restore_not_placed', 'bug'],
|
|
219
|
+
['a restored file’s folder was moved out of reach as it landed', 'trash.restore_folder_moved'],
|
|
220
|
+
['the daily storage cleanup failed', 'trash.cleanup_failed', 'bug'],
|
|
221
|
+
['a trash item whose meta.json is unreadable or not valid was kept as it is', 'trash.unreadable_kept'],
|
|
222
|
+
['a trash item whose deletion time is in the future was kept as it is', 'trash.future_kept'],
|
|
223
|
+
['could not remove an unfinished trash item', 'trash.staging_left'],
|
|
224
|
+
['the inbox cleanup was skipped: files are moved to the trash only through folders held open (/proc/self/fd, on Linux)', 'trash.no_handles'],
|
|
225
|
+
['webhook', 'webhook.request', undefined, 'webhook request'],
|
|
226
|
+
['webhook caller hung up before the answer', 'webhook.caller_left'],
|
|
227
|
+
['webhook redelivery of a run that did not succeed: running it again', 'webhook.redelivery'],
|
|
228
|
+
['work_feed events without a usable cursor or job id were skipped', 'work.feed_events_skipped'],
|
|
229
|
+
['work started through a connector', 'work.started'],
|
|
230
|
+
['work_start returned a job another Boton started; it stays theirs', 'work.job_theirs'],
|
|
231
|
+
['work update for a job no Boton is known to have started: not delivered', 'work.update_undelivered'],
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
interface Event {
|
|
235
|
+
event: string;
|
|
236
|
+
label: string;
|
|
237
|
+
help: RecentLogHelp | null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const CATALOG: ReadonlyMap<string, Event> = new Map(EVENTS.map(([message, event, help, label]) => [message, { event, label: label ?? message, help: help ?? null }]));
|
|
241
|
+
|
|
242
|
+
/** For the test that keeps EVENTS in step with the messages in server/src. */
|
|
243
|
+
export const CATALOG_MESSAGES: readonly string[] = EVENTS.map(([m]) => m);
|
|
244
|
+
|
|
245
|
+
/** The event a message is: an exact match on a short primitive string, or unclassified. Never reads a long text. */
|
|
246
|
+
export function classify(msg: unknown): Event {
|
|
247
|
+
if (typeof msg !== 'string' || msg.length > MESSAGE_MAX) return { ...UNCLASSIFIED, help: null };
|
|
248
|
+
return CATALOG.get(msg) ?? { ...UNCLASSIFIED, help: null };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---- fields ----
|
|
252
|
+
|
|
253
|
+
type Validator = (v: unknown) => RecentLogField | undefined;
|
|
254
|
+
|
|
255
|
+
/** One of the server's own ids (`newId` in util.ts): its prefix, then a fixed number of [0-9a-z]. */
|
|
256
|
+
const id = (prefix: string, length = 8): Validator => {
|
|
257
|
+
const re = new RegExp(`^${prefix}_[0-9a-z]{${length}}$`);
|
|
258
|
+
const size = prefix.length + 1 + length;
|
|
259
|
+
return (v) => (typeof v === 'string' && v.length === size && re.test(v) ? v : undefined);
|
|
260
|
+
};
|
|
261
|
+
const oneOf = (...words: string[]): Validator => {
|
|
262
|
+
const set = new Set(words);
|
|
263
|
+
return (v) => (typeof v === 'string' && v.length <= 32 && set.has(v) ? v : undefined);
|
|
264
|
+
};
|
|
265
|
+
const whole = (min: number, max: number): Validator => (v) => (typeof v === 'number' && Number.isSafeInteger(v) && v >= min && v <= max ? v : undefined);
|
|
266
|
+
const flag: Validator = (v) => (typeof v === 'boolean' ? v : undefined);
|
|
267
|
+
const either = (a: Validator, b: Validator): Validator => (v) => a(v) ?? b(v);
|
|
268
|
+
|
|
269
|
+
const COUNT = whole(0, 1_000_000_000);
|
|
270
|
+
const MS = whole(0, 1_000_000_000_000);
|
|
271
|
+
|
|
272
|
+
/** The only names read from a line's fields, and what each may hold. */
|
|
273
|
+
const FIELDS: Readonly<Record<string, Validator>> = {
|
|
274
|
+
botId: id('bot'),
|
|
275
|
+
fromBotId: id('bot'),
|
|
276
|
+
routineId: id('rt'),
|
|
277
|
+
runId: id('run'),
|
|
278
|
+
retryOf: id('run'),
|
|
279
|
+
approvalId: id('apr'),
|
|
280
|
+
connectionId: id('oac'),
|
|
281
|
+
threadId: id('thr'),
|
|
282
|
+
deviceId: id('dev'),
|
|
283
|
+
templateId: id('tpl'),
|
|
284
|
+
provider: oneOf('claude-cli', 'codex-cli', 'mock'),
|
|
285
|
+
api: oneOf('anthropic', 'openai'),
|
|
286
|
+
effort: oneOf(...EFFORT_IDS),
|
|
287
|
+
trigger: oneOf('chat', 'schedule', 'webhook', 'manual', 'test'),
|
|
288
|
+
kind: oneOf('chat', 'routine'),
|
|
289
|
+
outcome: oneOf('running', 'ok', 'error', 'timeout', 'stopped', 'interrupted', 'granted', 'pending'),
|
|
290
|
+
decision: oneOf('allow', 'deny'),
|
|
291
|
+
session: oneOf('new', 'resume'),
|
|
292
|
+
was: oneOf('queued', 'running'),
|
|
293
|
+
status: either(oneOf('queued', 'running', 'succeeded', 'failed', 'skipped'), whole(100, 599)),
|
|
294
|
+
method: oneOf('GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'),
|
|
295
|
+
channel: oneOf('ntfy'),
|
|
296
|
+
origin: oneOf('uncaughtException', 'unhandledRejection'),
|
|
297
|
+
signal: oneOf('SIGTERM', 'SIGKILL', 'SIGINT', 'SIGHUP', 'SIGQUIT', 'SIGABRT'),
|
|
298
|
+
limit: either(oneOf('pair', 'total'), COUNT),
|
|
299
|
+
ok: flag,
|
|
300
|
+
headless: flag,
|
|
301
|
+
sandbox: flag,
|
|
302
|
+
always: flag,
|
|
303
|
+
inTurn: flag,
|
|
304
|
+
duplicate: flag,
|
|
305
|
+
rotated: flag,
|
|
306
|
+
timedOut: flag,
|
|
307
|
+
sessionLost: flag,
|
|
308
|
+
workFeed: flag,
|
|
309
|
+
exitCode: whole(-1_000_000, 1_000_000),
|
|
310
|
+
durationMs: MS,
|
|
311
|
+
timeLimitMs: MS,
|
|
312
|
+
retryInMs: MS,
|
|
313
|
+
attempt: COUNT,
|
|
314
|
+
attempts: COUNT,
|
|
315
|
+
routines: COUNT,
|
|
316
|
+
tools: COUNT,
|
|
317
|
+
sites: COUNT,
|
|
318
|
+
messages: COUNT,
|
|
319
|
+
max: COUNT,
|
|
320
|
+
hop: COUNT,
|
|
321
|
+
size: COUNT,
|
|
322
|
+
interrupted: COUNT,
|
|
323
|
+
skipped: COUNT,
|
|
324
|
+
};
|
|
325
|
+
const FIELD_NAMES = Object.keys(FIELDS);
|
|
326
|
+
|
|
327
|
+
/** The standard OAuth error codes (RFC 6749, 6750): an `error` field that is exactly one of them is shown. */
|
|
328
|
+
const OAUTH_ERROR = oneOf(
|
|
329
|
+
'invalid_request', 'invalid_client', 'invalid_grant', 'unauthorized_client', 'unsupported_grant_type', 'invalid_scope',
|
|
330
|
+
'access_denied', 'unsupported_response_type', 'server_error', 'temporarily_unavailable', 'invalid_token', 'insufficient_scope',
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
/** What a thrown value's own `code` says, as a fixed category. */
|
|
334
|
+
const ERROR_CODES: Readonly<Record<string, string>> = {
|
|
335
|
+
ECONNREFUSED: 'connection-refused',
|
|
336
|
+
ECONNRESET: 'connection-reset',
|
|
337
|
+
EPIPE: 'connection-reset',
|
|
338
|
+
ETIMEDOUT: 'timeout',
|
|
339
|
+
ESOCKETTIMEDOUT: 'timeout',
|
|
340
|
+
UND_ERR_CONNECT_TIMEOUT: 'timeout',
|
|
341
|
+
UND_ERR_HEADERS_TIMEOUT: 'timeout',
|
|
342
|
+
UND_ERR_BODY_TIMEOUT: 'timeout',
|
|
343
|
+
ABORT_ERR: 'aborted',
|
|
344
|
+
ENOTFOUND: 'dns',
|
|
345
|
+
EAI_AGAIN: 'dns',
|
|
346
|
+
EHOSTUNREACH: 'unreachable',
|
|
347
|
+
ENETUNREACH: 'unreachable',
|
|
348
|
+
UND_ERR_SOCKET: 'connection-reset',
|
|
349
|
+
CERT_HAS_EXPIRED: 'tls',
|
|
350
|
+
UNABLE_TO_VERIFY_LEAF_SIGNATURE: 'tls',
|
|
351
|
+
DEPTH_ZERO_SELF_SIGNED_CERT: 'tls',
|
|
352
|
+
ERR_TLS_CERT_ALTNAME_INVALID: 'tls',
|
|
353
|
+
EACCES: 'permission',
|
|
354
|
+
EPERM: 'permission',
|
|
355
|
+
ENOENT: 'not-found',
|
|
356
|
+
ENOSPC: 'disk-full',
|
|
357
|
+
EDQUOT: 'disk-full',
|
|
358
|
+
EADDRINUSE: 'address-in-use',
|
|
359
|
+
EADDRNOTAVAIL: 'address-unavailable',
|
|
360
|
+
EMFILE: 'too-many-files',
|
|
361
|
+
ENFILE: 'too-many-files',
|
|
362
|
+
SQLITE_BUSY: 'database-busy',
|
|
363
|
+
SQLITE_FULL: 'disk-full',
|
|
364
|
+
SQLITE_CORRUPT: 'database-corrupt',
|
|
365
|
+
ERR_SQLITE_ERROR: 'database',
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/** An own data property's value, or undefined: never a getter, a proxy trap or anything on the prototype. */
|
|
369
|
+
function ownValue(o: unknown, key: string): unknown {
|
|
370
|
+
if (typeof o !== 'object' || o === null || types.isProxy(o)) return undefined;
|
|
371
|
+
let d: PropertyDescriptor | undefined;
|
|
372
|
+
try {
|
|
373
|
+
d = Object.getOwnPropertyDescriptor(o, key);
|
|
374
|
+
} catch {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
return d && 'value' in d ? d.value : undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function codeCategory(o: unknown): string | undefined {
|
|
381
|
+
const code = ownValue(o, 'code');
|
|
382
|
+
return typeof code === 'string' && code.length <= 40 && Object.hasOwn(ERROR_CODES, code) ? ERROR_CODES[code] : undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** A thrown value as a category: its own `code`, else its cause's; 'other' when it says nothing known. */
|
|
386
|
+
export function errorCategory(err: unknown): string {
|
|
387
|
+
return codeCategory(err) ?? codeCategory(ownValue(err, 'cause')) ?? 'other';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** The kept fields of a line: only FIELDS (and `err`, `error`), each validated, read without running any code. */
|
|
391
|
+
export function selectFields(fields: unknown): Record<string, RecentLogField> {
|
|
392
|
+
const out: Record<string, RecentLogField> = {};
|
|
393
|
+
if (typeof fields !== 'object' || fields === null || types.isProxy(fields)) return out;
|
|
394
|
+
for (const name of FIELD_NAMES) {
|
|
395
|
+
const v = FIELDS[name](ownValue(fields, name));
|
|
396
|
+
if (v !== undefined) out[name] = v;
|
|
397
|
+
}
|
|
398
|
+
const err = ownValue(fields, 'err');
|
|
399
|
+
if (err !== undefined && err !== null) out.errorCategory = errorCategory(err);
|
|
400
|
+
const oauth = OAUTH_ERROR(ownValue(fields, 'error'));
|
|
401
|
+
if (oauth !== undefined) out.oauthError = oauth;
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ---- the buffer ----
|
|
406
|
+
|
|
407
|
+
export class RecentLogs {
|
|
408
|
+
#entries: RecentLogEntry[] = [];
|
|
409
|
+
#sizes: number[] = [];
|
|
410
|
+
#bytes = 0;
|
|
411
|
+
#seq = 0;
|
|
412
|
+
#dropped = 0;
|
|
413
|
+
#since = new Date().toISOString();
|
|
414
|
+
#threshold: () => LogLevel;
|
|
415
|
+
#untap: (() => void) | null = null;
|
|
416
|
+
|
|
417
|
+
constructor(opts: { threshold: () => LogLevel }) {
|
|
418
|
+
this.#threshold = opts.threshold;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Starts seeing the server's log lines; stop() ends it. */
|
|
422
|
+
start(): this {
|
|
423
|
+
this.#untap ??= tapLogs((level, msg, fields, at) => this.add(level, msg, fields, at));
|
|
424
|
+
return this;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
stop(): void {
|
|
428
|
+
this.#untap?.();
|
|
429
|
+
this.#untap = null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
add(level: LogLevel, msg: unknown, fields: LogFields | unknown, at = new Date()): void {
|
|
433
|
+
if (level !== 'info' && level !== 'warn' && level !== 'error') return;
|
|
434
|
+
const { event, label, help } = classify(msg);
|
|
435
|
+
const entry: RecentLogEntry = { seq: ++this.#seq, ts: at.toISOString(), level, event, label, help, fields: selectFields(fields) };
|
|
436
|
+
const size = Buffer.byteLength(JSON.stringify(entry), 'utf8');
|
|
437
|
+
this.#entries.push(entry);
|
|
438
|
+
this.#sizes.push(size);
|
|
439
|
+
this.#bytes += size;
|
|
440
|
+
while (this.#entries.length > RECENT_LOG_ENTRIES || (this.#bytes > RECENT_LOG_BYTES && this.#entries.length > 1)) {
|
|
441
|
+
this.#entries.shift();
|
|
442
|
+
this.#bytes -= this.#sizes.shift() ?? 0;
|
|
443
|
+
this.#dropped++;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Newest first. `level` is the least severe kept; `q` matches the label, event code and field values, ignoring case. */
|
|
448
|
+
list(opts: { level?: string | null; q?: string | null; boton?: string | null; before?: number | null; limit?: number | null } = {}): RecentLogsAnswer {
|
|
449
|
+
const min = LEVELS.indexOf((LEVELS as readonly string[]).includes(opts.level ?? '') ? (opts.level as RecentLogLevel) : 'info');
|
|
450
|
+
const q = (opts.q ?? '').slice(0, QUERY_MAX).trim().toLowerCase();
|
|
451
|
+
const boton = (opts.boton ?? '').trim();
|
|
452
|
+
const before = opts.before && opts.before > 0 ? opts.before : Number.POSITIVE_INFINITY;
|
|
453
|
+
const limit = Math.min(Math.max(Math.trunc(opts.limit ?? 100) || 100, 1), RECENT_LOG_PAGE);
|
|
454
|
+
const counts: Record<RecentLogLevel, number> = { info: 0, warn: 0, error: 0 };
|
|
455
|
+
const entries: RecentLogEntry[] = [];
|
|
456
|
+
let more = false;
|
|
457
|
+
for (let i = this.#entries.length - 1; i >= 0; i--) {
|
|
458
|
+
const e = this.#entries[i];
|
|
459
|
+
counts[e.level]++;
|
|
460
|
+
if (e.seq >= before || LEVELS.indexOf(e.level) < min) continue;
|
|
461
|
+
if (boton && e.fields.botId !== boton && e.fields.fromBotId !== boton) continue;
|
|
462
|
+
if (q && !matches(e, q)) continue;
|
|
463
|
+
if (entries.length >= limit) {
|
|
464
|
+
more = true;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
entries.push(e);
|
|
468
|
+
}
|
|
469
|
+
const threshold = this.#threshold();
|
|
470
|
+
return {
|
|
471
|
+
entries,
|
|
472
|
+
more,
|
|
473
|
+
since: this.#since,
|
|
474
|
+
oldestAt: this.#entries[0]?.ts ?? null,
|
|
475
|
+
dropped: this.#dropped,
|
|
476
|
+
kept: this.#entries.length,
|
|
477
|
+
counts,
|
|
478
|
+
bytes: this.#bytes,
|
|
479
|
+
limits: { entries: RECENT_LOG_ENTRIES, bytes: RECENT_LOG_BYTES, page: RECENT_LOG_PAGE },
|
|
480
|
+
threshold: threshold === 'debug' ? 'info' : threshold,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function matches(e: RecentLogEntry, q: string): boolean {
|
|
486
|
+
if (e.label.toLowerCase().includes(q) || e.event.includes(q)) return true;
|
|
487
|
+
for (const v of Object.values(e.fields)) if (String(v).toLowerCase().includes(q)) return true;
|
|
488
|
+
return false;
|
|
489
|
+
}
|