@piagent/platform 1.34.2 → 1.35.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/README.vi.md +1 -1
  4. package/docs/command-reference-vietnamese.md +2 -2
  5. package/docs/distribution-standard.md +6 -6
  6. package/docs/languages.json +2 -2
  7. package/docs/operator-manual-vietnamese.md +7 -7
  8. package/docs/publishing-for-teams.md +2 -0
  9. package/docs/release-install-policy.md +2 -2
  10. package/docs/team-onboarding.md +2 -2
  11. package/docs-site/architecture.html +1 -1
  12. package/docs-site/benchmark.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/content/en/index.html +4 -4
  15. package/docs-site/content/en/whats-new.html +12 -0
  16. package/docs-site/content/index.html +4 -4
  17. package/docs-site/content/whats-new.html +12 -0
  18. package/docs-site/context-engine.html +1 -1
  19. package/docs-site/ecosystem.html +1 -1
  20. package/docs-site/en/architecture.html +1 -1
  21. package/docs-site/en/benchmark.html +1 -1
  22. package/docs-site/en/commands.html +1 -1
  23. package/docs-site/en/context-engine.html +1 -1
  24. package/docs-site/en/ecosystem.html +1 -1
  25. package/docs-site/en/index.html +5 -5
  26. package/docs-site/en/mcp-approval.html +1 -1
  27. package/docs-site/en/mcp-auth.html +1 -1
  28. package/docs-site/en/mcp-servers.html +1 -1
  29. package/docs-site/en/mcp.html +1 -1
  30. package/docs-site/en/permissions.html +1 -1
  31. package/docs-site/en/profiles.html +1 -1
  32. package/docs-site/en/quickstart.html +1 -1
  33. package/docs-site/en/runtime.html +1 -1
  34. package/docs-site/en/scope.html +1 -1
  35. package/docs-site/en/security.html +1 -1
  36. package/docs-site/en/team.html +1 -1
  37. package/docs-site/en/whats-new.html +13 -1
  38. package/docs-site/en/windows.html +1 -1
  39. package/docs-site/en/workflows.html +1 -1
  40. package/docs-site/index.html +5 -5
  41. package/docs-site/mcp-approval.html +1 -1
  42. package/docs-site/mcp-auth.html +1 -1
  43. package/docs-site/mcp-servers.html +1 -1
  44. package/docs-site/mcp.html +1 -1
  45. package/docs-site/permissions.html +1 -1
  46. package/docs-site/profiles.html +1 -1
  47. package/docs-site/quickstart.html +1 -1
  48. package/docs-site/runtime.html +1 -1
  49. package/docs-site/scope.html +1 -1
  50. package/docs-site/security.html +1 -1
  51. package/docs-site/team.html +1 -1
  52. package/docs-site/whats-new.html +13 -1
  53. package/docs-site/windows.html +1 -1
  54. package/docs-site/workflows.html +1 -1
  55. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  56. package/package.json +1 -1
  57. package/packages/piagent-core/managed/continuation.mjs +13 -2
  58. package/packages/piagent-core/managed/request-stream.mjs +42 -7
  59. package/packages/piagent-core/managed/session.mjs +4 -4
  60. package/packages/piagent-core/managed/studio-link.mjs +223 -0
  61. package/packages/piagent-core/package.json +1 -1
  62. package/packages/piagent-core/runtime/managed-failure.mjs +5 -1
  63. package/packages/piagent-webui/gateway/gateway-service.ts +5 -1
  64. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +6 -4
@@ -0,0 +1,223 @@
1
+ // The member's link to Studio while a company conversation runs. A long task
2
+ // (a checklist left running overnight) outlives Studio: an update restarts it,
3
+ // the office network drops, Agent Watch reconnects. When a request cannot
4
+ // reach Studio for a reason that is Studio's or the way to it (not the key,
5
+ // the quota or a policy), the work waits here, reading Studio's heartbeat,
6
+ // and goes on once Studio answers again: no limit on how long, only Stop ends
7
+ // the wait. Each wait is written to a local log (one JSON line per event,
8
+ // filterable by `kind` and `cause`) and reported to Studio
9
+ // (POST /studio/v1/managed/disconnects), which decides the cause from its own
10
+ // starts: an update (a new version), a restart (a new start, same version),
11
+ // or the member's network.
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { randomUUID } from 'node:crypto';
16
+ import { setTimeout as sleep } from 'node:timers/promises';
17
+ import { CLIENT_AGENT } from './client-agent.mjs';
18
+
19
+ // Codes whose cause is Studio or the way to it: Studio, its gateway or its
20
+ // connector did not answer (stopped, restarting, an update), or Agent Watch
21
+ // lost Studio or stopped. Everything else fails as before.
22
+ export const STUDIO_WAIT_CODES = new Set(['studio-unreachable', 'studio-gateway-unavailable', 'connector_unavailable', 'inference_unavailable',
23
+ 'operation_unavailable', 'managed-broker:offline', 'managed-broker-timeout', 'managed-broker-disconnected',
24
+ 'managed-broker:serverUnavailable', 'managed-broker:upstreamUnavailable']);
25
+ const BROKER = /^managed-broker/;
26
+ // Answered by Studio itself or by Agent Watch on this machine: Studio's own
27
+ // failure only while Studio is away or just back (its connector or Agent
28
+ // Watch reconnect after an update). With Studio up all along they fail at
29
+ // once, as before (a connector down on a running Studio is not waited out).
30
+ const WHEN_STUDIO_AWAY = new Set(['connector_unavailable', 'inference_unavailable', 'operation_unavailable']);
31
+ const conditional = code => BROKER.test(code) || WHEN_STUDIO_AWAY.has(code);
32
+ export const LINK_LOG = 'studio-link.jsonl';
33
+ const LOG_BYTES = 2 * 1024 * 1024, QUEUE = 50, BATCH = 20, SEND_TRIES = 5;
34
+ const MIN_PAUSE_MS = 2_000, MAX_PAUSE_MS = 30_000, MAX_STREAK_PAUSE_MS = 5 * 60_000, BEAT_TIMEOUT_MS = 5_000;
35
+ // After Studio was away, Agent Watch gets this long to reach it again.
36
+ export const BROKER_GRACE_MS = 10 * 60_000;
37
+
38
+ const header = (headers, name) => {
39
+ if (!headers) return null;
40
+ const value = typeof headers.get === 'function' ? headers.get(name) : headers[name] ?? headers[name.toLowerCase()];
41
+ return typeof value === 'string' && value.trim() ? value.trim().slice(0, 64) : null;
42
+ };
43
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
44
+ const VERSION = /^[A-Za-z0-9][A-Za-z0-9.+_-]{0,39}$/;
45
+ const clean = (boot, version) => ({ boot: UUID.test(boot ?? '') ? boot : null, version: VERSION.test(version ?? '') ? version : null });
46
+
47
+ // What Studio's starts and the heartbeat's answers say about a wait. A
48
+ // Studio that started during the wait restarted, even when the conversation
49
+ // never saw its version before (a Studio older than the heartbeat).
50
+ export function classifyWait(before, after, seen, codes, since = null) {
51
+ if (before?.version && after?.version && before.version !== after.version) return 'studio_update';
52
+ if (before?.boot && after?.boot && before.boot !== after.boot) return 'studio_restart';
53
+ if (!before?.boot && since !== null && Date.parse(after?.startedAt ?? '') >= since - 60_000) return 'studio_restart';
54
+ if (seen.has('draining') || seen.has('starting')) return 'studio_unavailable';
55
+ if (codes.length && codes.every(c => BROKER.test(c))) return 'broker_offline';
56
+ if (seen.has('down')) return 'network';
57
+ return 'unknown';
58
+ }
59
+
60
+ export class StudioLink {
61
+ constructor({ origin, agentDir = null, notify = () => {}, ui = () => null, fetchImpl = (...a) => globalThis.fetch(...a), now = () => Date.now(),
62
+ pause = (ms, signal) => sleep(ms, undefined, { signal }), random = Math.random, minPauseMs = MIN_PAUSE_MS, maxPauseMs = MAX_PAUSE_MS } = {}) {
63
+ Object.assign(this, { origin, notify, ui, fetchImpl, now, pause, random, minPauseMs, maxPauseMs });
64
+ this.logFile = agentDir ? path.join(agentDir, 'logs', LINK_LOG) : null;
65
+ this.last = { boot: null, version: null };
66
+ // What Studio was before a wait comes from its answers' headers (observe):
67
+ // no extra request when a conversation opens.
68
+ this.queue = []; this.waiting = new Set(); this.streak = 0; this.disabled = false; this.sending = null;
69
+ }
70
+
71
+ // Studio's start and version from any of its answers (X-Studio-Boot, X-Studio-Version).
72
+ observe(headers) {
73
+ const seen = clean(header(headers, 'x-studio-boot'), header(headers, 'x-studio-version'));
74
+ if (seen.boot) this.last = { boot: seen.boot, version: seen.version ?? this.last.version };
75
+ }
76
+ // A request went through: the next wait starts with a short pause again.
77
+ ok() { this.streak = 0; }
78
+ waits(code) { return STUDIO_WAIT_CODES.has(code); }
79
+ // Studio or the way to it, whatever Studio's heartbeat says now.
80
+ always(code) { return this.waits(code) && !conditional(code); }
81
+
82
+ // GET /v1/heartbeat: ready, starting, draining, or down (no answer, or a
83
+ // gateway's page instead of Studio's).
84
+ async beat(signal) {
85
+ const timeout = AbortSignal.timeout(BEAT_TIMEOUT_MS), both = signal ? AbortSignal.any([signal, timeout]) : timeout;
86
+ try {
87
+ const response = await this.fetchImpl(`${this.origin}/v1/heartbeat`, { headers: { 'User-Agent': CLIENT_AGENT, Accept: 'application/json' }, signal: both });
88
+ // A Studio older than the heartbeat answers 404: it answers, so it is up.
89
+ if (response.status === 404 && header(response.headers, 'x-request-id')) { void response.body?.cancel?.().catch(() => {}); return { state: 'ready', boot: null, version: null }; }
90
+ const body = await response.json().catch(() => null);
91
+ const state = ['ready', 'starting', 'draining'].includes(body?.status) ? body.status : 'down';
92
+ const seen = clean(body?.boot_id, body?.version), startedAt = typeof body?.started_at === 'string' ? body.started_at.slice(0, 40) : null;
93
+ if (state === 'ready' && response.ok && seen.boot) this.last = seen;
94
+ return { state: state === 'ready' && !response.ok ? 'down' : state, ...seen, startedAt };
95
+ } catch (error) {
96
+ if (signal?.aborted) throw error;
97
+ return { state: 'down', boot: null, version: null };
98
+ }
99
+ }
100
+
101
+ // Waits until Studio answers its heartbeat as ready, then returns what the
102
+ // wait was; throws (AbortError) when the member stops the work meanwhile.
103
+ // Null: nothing to wait for. Agent Watch lost Studio, or Studio's
104
+ // connector did not answer, while Studio answers here and was not away
105
+ // lately: that fails at once as before.
106
+ async waitUntilBack({ phase = 'model', code = 'studio-unreachable', role = 'main', conversation = null, runId = null, signal } = {}) {
107
+ if (signal?.aborted) throw signal.reason ?? Error('aborted');
108
+ if (!this.waits(code)) return null;
109
+ const first = conditional(code) ? await this.beat(signal) : null;
110
+ if (first?.state === 'ready' && !this.justBack()) return null;
111
+ const controller = new AbortController(), stop = () => controller.abort(signal?.reason);
112
+ signal?.addEventListener('abort', stop, { once: true });
113
+ this.waiting.add(controller);
114
+ const event = { id: randomUUID(), phase, role, codes: [code], started: this.now(), before: { ...this.last }, attempts: 0, conversation, runId };
115
+ const seen = new Set(first ? [first.state] : []);
116
+ this.log({ kind: 'studio_wait', state: 'start', id: event.id, phase, role, code, version: event.before.version, conversation });
117
+ this.notify({ type: 'managed_studio_wait', id: event.id, role, phase, code, state: 'start', waitedMs: 0 });
118
+ this.status(`Studio is not answering (${code}). Waiting; the work goes on by itself when Studio is back. Esc stops.`);
119
+ // Failing again right after a wait (Studio answers, the request does
120
+ // not): each such wait starts with a longer pause, up to five minutes.
121
+ let pause = Math.min(MAX_STREAK_PAUSE_MS, this.minPauseMs * 2 ** Math.min(this.streak, 8));
122
+ let after = null;
123
+ try {
124
+ for (;;) {
125
+ await this.pause(Math.round(pause * (0.75 + this.random() / 2)), controller.signal);
126
+ event.attempts += 1;
127
+ after = await this.beat(controller.signal);
128
+ seen.add(after.state);
129
+ if (after.state === 'ready') break;
130
+ this.notify({ type: 'managed_studio_wait', id: event.id, role, phase, code, state: 'waiting', studio: after.state, waitedMs: this.now() - event.started });
131
+ this.status(`Studio is ${after.state === 'down' ? 'out of reach' : after.state} (${Math.round((this.now() - event.started) / 60_000)} min). The work goes on by itself when Studio is back. Esc stops.`);
132
+ pause = Math.min(this.maxPauseMs, pause * 2);
133
+ }
134
+ this.streak += 1;
135
+ return this.ended(event, 'resumed', after, seen);
136
+ } catch (error) {
137
+ this.ended(event, 'stopped', after, seen);
138
+ throw error;
139
+ } finally {
140
+ signal?.removeEventListener('abort', stop);
141
+ this.waiting.delete(controller);
142
+ if (!this.waiting.size) this.status(undefined);
143
+ }
144
+ }
145
+
146
+ // Whether a failure with this code waits for Studio (see waitUntilBack).
147
+ async needed(code, signal) {
148
+ if (signal?.aborted) throw signal.reason ?? Error('aborted');
149
+ if (!this.waits(code)) return false;
150
+ return !(conditional(code) && (await this.beat(signal)).state === 'ready' && !this.justBack());
151
+ }
152
+ justBack() { return !!this.recoveredAt && this.now() - this.recoveredAt < (this.brokerGraceMs ?? BROKER_GRACE_MS); }
153
+
154
+ // Stop ends every wait at once.
155
+ cancel() { for (const controller of this.waiting) controller.abort(Error('managed-request-aborted')); }
156
+
157
+ ended(event, outcome, after, seen) {
158
+ const ended = this.now(), cause = classifyWait(event.before, after ?? {}, seen, event.codes, event.started);
159
+ // Studio itself was away: Agent Watch gets BROKER_GRACE_MS to reach it again.
160
+ if (outcome === 'resumed' && [...seen].some(state => state !== 'ready')) this.recoveredAt = ended;
161
+ const record = { kind: 'studio_disconnect', id: event.id, phase: event.phase, role: event.role, cause, outcome, attempts: event.attempts, codes: event.codes,
162
+ started_at: new Date(event.started).toISOString(), ended_at: new Date(ended).toISOString(), waited_ms: ended - event.started,
163
+ version_before: event.before.version, version_after: after?.version ?? null, boot_before: event.before.boot, boot_after: after?.boot ?? null,
164
+ conversation: event.conversation, run_id: event.runId };
165
+ this.log(record);
166
+ this.notify({ type: 'managed_studio_wait', id: event.id, role: event.role, phase: event.phase, code: event.codes[0], state: 'end', outcome, cause, waitedMs: record.waited_ms });
167
+ if (this.queue.length >= QUEUE) this.queue.shift();
168
+ this.queue.push({ record, tries: 0 });
169
+ return record;
170
+ }
171
+
172
+ // The waits Studio has not heard of yet, sent with a token that works now.
173
+ // A Studio without the route (404) is not asked again in this conversation.
174
+ async flush(token) {
175
+ if (this.disabled || !token || !this.queue.length || this.sending) return this.sending ?? false;
176
+ const batch = this.queue.slice(0, BATCH), at = this.now();
177
+ const events = batch.map(({ record: r }) => ({ id: r.id, phase: r.phase, cause: r.cause, outcome: r.outcome, attempts: Math.min(r.attempts, 1_000_000), codes: r.codes.slice(0, 8),
178
+ started_age_ms: Math.max(0, at - Date.parse(r.started_at)), ended_age_ms: Math.max(0, at - Date.parse(r.ended_at)),
179
+ ...(r.conversation && /^[A-Za-z0-9_-]{1,80}$/.test(r.conversation) ? { conversation: r.conversation } : {}), ...(UUID.test(r.run_id ?? '') ? { run_id: r.run_id } : {}),
180
+ ...(r.boot_before ? { boot_before: r.boot_before } : {}), ...(r.boot_after ? { boot_after: r.boot_after } : {}),
181
+ ...(r.version_before ? { version_before: r.version_before } : {}), ...(r.version_after ? { version_after: r.version_after } : {}) }))
182
+ .filter(e => e.started_age_ms <= 7 * 24 * 3600_000);
183
+ const done = sent => { this.queue = this.queue.filter(q => !batch.includes(q) || (!sent && ++q.tries < SEND_TRIES)); return sent; };
184
+ if (!events.length) return done(true);
185
+ this.sending = this.fetchImpl(`${this.origin}/studio/v1/managed/disconnects`, { method: 'POST', signal: AbortSignal.timeout(10_000),
186
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', 'User-Agent': CLIENT_AGENT },
187
+ body: JSON.stringify({ os: `${os.platform()} ${os.release()} ${os.arch()}`.slice(0, 80), events }) })
188
+ .then(response => {
189
+ void response.body?.cancel?.().catch(() => {});
190
+ if (response.status === 404) { this.disabled = true; return done(false); }
191
+ return done(response.status === 204);
192
+ }, () => done(false))
193
+ .finally(() => { this.sending = null; });
194
+ return this.sending;
195
+ }
196
+
197
+ // One JSON line per event in <agentDir>/logs/studio-link.jsonl (kept to
198
+ // about 2 MB, the previous file as .1): `jq 'select(.cause=="studio_update")'`.
199
+ log(entry) {
200
+ if (!this.logFile) return;
201
+ try {
202
+ fs.mkdirSync(path.dirname(this.logFile), { recursive: true, mode: 0o700 });
203
+ try { if (fs.statSync(this.logFile).size > LOG_BYTES) fs.renameSync(this.logFile, `${this.logFile}.1`); } catch { /* no log yet */ }
204
+ fs.appendFileSync(this.logFile, `${JSON.stringify({ at: new Date(this.now()).toISOString(), ...entry })}\n`, { mode: 0o600 });
205
+ } catch { /* the wait goes on without its log line */ }
206
+ }
207
+
208
+ // The Terminal's working line while anything waits for Studio.
209
+ status(text) {
210
+ try { this.ui()?.setWorkingMessage?.(text); } catch { /* no Terminal */ }
211
+ }
212
+ }
213
+
214
+ // What the main agent reads when its work goes on after Studio was back.
215
+ export function resumeMessage(wait, code) {
216
+ const why = { studio_update: `Studio was updated${wait.version_before && wait.version_after ? ` (${wait.version_before} → ${wait.version_after})` : ''}`,
217
+ studio_restart: 'Studio restarted', studio_unavailable: 'Studio was not ready', network: 'the connection to Studio dropped',
218
+ broker_offline: 'Agent Watch lost Studio', unknown: 'Studio could not be reached' }[wait.cause] ?? 'Studio could not be reached';
219
+ const minutes = Math.max(1, Math.round(wait.waited_ms / 60_000));
220
+ return { customType: 'agent-watch-process', display: true,
221
+ content: `Harness: your last step was cut off because ${why} [${code}]; Studio answers again after about ${minutes} min. Nothing you asked for is lost: the files and the checklist are as you left them. Check where you were (the checklist, git status, the last command you ran) and continue the member's task from there; do not redo work that is already done.`,
222
+ details: { phase: 'resume', cause: wait.cause, waitedMs: wait.waited_ms, code } };
223
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.34.2",
3
+ "version": "1.35.0",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -98,6 +98,9 @@ const CODES = {
98
98
  'managed-plan-invalid': ['tool', 'the checklist was not in the expected form'],
99
99
  'managed-request-failed': ['failed', 'the request could not be sent'],
100
100
  'studio-request-failed': ['failed', 'Studio answered with an error'],
101
+ // A gateway's page (Cloudflare, the LAN edge) instead of Studio's answer:
102
+ // Studio is stopped or restarting behind it, or the way to it failed.
103
+ 'studio-gateway-unavailable': ['unreachable', "Studio's gateway answered that Studio is not reachable"],
101
104
  };
102
105
  const ROLES = { main: 'main agent', scout: 'scout subagent', research: 'research subagent', verify: 'verify subagent', review: 'review subagent' };
103
106
  const REQUEST = /\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/;
@@ -133,7 +136,7 @@ export function redactedText(raw) { return redactSensitiveText(String(raw ?? '')
133
136
  // The sentence alone, for a status line that already says who is waiting.
134
137
  export function failureReason(code) { return (CODES[code] ?? CODES['managed-request-failed'])[1]; }
135
138
  // A failure Studio never saw: nothing was sent, so it is not a model request.
136
- export function failureIsLocal(code) { return code.startsWith('managed-') || code === 'studio-unreachable'; }
139
+ export function failureIsLocal(code) { return code.startsWith('managed-') || code === 'studio-unreachable' || code === 'studio-gateway-unavailable'; }
137
140
 
138
141
  function known(code) { return typeof code === 'string' && Object.hasOwn(CODES, code) ? code : null; }
139
142
 
@@ -147,6 +150,7 @@ export function failureCode(raw) {
147
150
  if (known(studio)) return studio;
148
151
  const local = /(managed-broker:[A-Za-z_]{1,80}|managed-[a-z-]{1,80}|web-search-empty)/.exec(text)?.[1];
149
152
  if (known(local)) return local;
153
+ if (!studio && /\bAPI error \((?:50[234]|52\d|530)\)|^(?:50[234]|52\d|530)\b/.test(text)) return 'studio-gateway-unavailable';
150
154
  if (studio || /\bAPI error \(\d{3}\)|^\d{3} \{/.test(text)) return 'studio-request-failed';
151
155
  // The provider declined the request itself (its usage policy), not Studio:
152
156
  // the same request is declined again, so it is said in words.
@@ -1,6 +1,7 @@
1
1
  import { managedProjection } from './managed-projection.ts';
2
2
  import { randomBytes } from "node:crypto";
3
3
  import fs from "node:fs";
4
+ import os from "node:os";
4
5
  import path from "node:path";
5
6
 
6
7
  import { redactSensitiveText } from "../../piagent-core/security/sensitive-data.js";
@@ -230,7 +231,10 @@ export async function startPiagentGateway(options: {
230
231
  const hubCatalog = relay ? async () => relay!.merge(await readCatalog()) : readCatalog;
231
232
  const company = (sessionRef: unknown) => !!relay?.owns(sessionRef);
232
233
  // Each conversation's state for Agent Watch (agent-status-files.ts): this Gateway's own conversations only.
233
- statusFiles = new AgentStatusFiles(path.join(state.agentDir, "status", options.managed ? "company" : "dashboard"), async () => { const catalog = await readCatalog(); if (catalog.state !== "ready") throw new Error("catalog-not-ready"); return catalog; }, () => Boolean(options.managed)); statusFiles.start();
234
+ // Agent Watch reads the member's ~/.pi/agent/status. The dashboard's
235
+ // agentDir is that folder; the company Gateway's is its private session
236
+ // folder under Agent Watch, so it writes to the member's folder instead.
237
+ statusFiles = new AgentStatusFiles((options.managed ? path.join(os.homedir(), ".pi", "agent", "status", "company") : path.join(state.agentDir, "status", "dashboard")), async () => { const catalog = await readCatalog(); if (catalog.state !== "ready") throw new Error("catalog-not-ready"); return catalog; }, () => Boolean(options.managed)); statusFiles.start();
234
238
  const projectFolder = async (projectRef: string) => projects.resolve(projectRef)
235
239
  ?? (await runtimes!.listSessions()).find((info) => typeof info.cwd === "string" && projectRefForCwd(key, info.cwd) === projectRef)?.cwd
236
240
  ?? (relay ? (await relay.folders()).find((folder) => projectRefForCwd(key, folder) === projectRef) : undefined);
@@ -18,12 +18,14 @@ export function bestEffortUnsubscribe(unsubscribe: (() => void) | null): void {
18
18
  }
19
19
 
20
20
  // A silent tool/provider gets 15 minutes. Any observed host event refreshes that
21
- // allowance, while the six-hour ceiling prevents an endless stream of progress
22
- // events from retaining runtime authority forever. Cleanup itself is separately
23
- // bounded so Stop and watchdog recovery cannot inherit the original hang.
21
+ // allowance, while the 24-hour ceiling prevents an endless stream of progress
22
+ // events from retaining runtime authority forever (a checklist left running
23
+ // overnight, waiting out a Studio update, takes 10 hours and more). Cleanup
24
+ // itself is separately bounded so Stop and watchdog recovery cannot inherit
25
+ // the original hang.
24
26
  const DEFAULT_POLICY: SessionOperationDeadlinePolicy = Object.freeze({
25
27
  inactivityTimeoutMs: 15 * 60_000,
26
- maximumDurationMs: 6 * 60 * 60_000,
28
+ maximumDurationMs: 24 * 60 * 60_000,
27
29
  terminationTimeoutMs: 5_000,
28
30
  projectionTimeoutMs: 2_500
29
31
  });