@stage-labs/metro 0.1.0-beta.307 → 0.1.0-beta.309
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/dist/agent.js +23 -28
- package/dist/child.js +33 -0
- package/dist/claude-env.js +104 -0
- package/dist/cli.js +7 -21
- package/dist/lock.js +62 -0
- package/dist/memory.js +12 -27
- package/dist/root-release-io.js +1 -2
- package/dist/root-release.js +1 -1
- package/dist/route.js +0 -1
- package/hooks/guard.mjs +1 -5
- package/hooks/session-start.mjs +1 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +13 -38
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +21 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -9
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +14 -12
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +6 -18
- package/runtime/node_modules/@metro-labs/daemon/src/claude/github-skills.ts +3 -7
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +6 -5
- package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +1 -23
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +39 -118
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup-api.ts +14 -61
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +10 -42
- package/runtime/node_modules/@metro-labs/daemon/src/claude/usage-probe.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +1 -6
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/claude-logins.ts +53 -29
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/claude-store.ts +55 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +40 -62
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/delivered.ts +82 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/effective.ts +47 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/fallback.ts +18 -19
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +37 -77
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +86 -220
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +80 -283
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-import.ts +125 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-lists.ts +0 -17
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-v2.ts +79 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +0 -79
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/org-models.ts +231 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +4 -44
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage-refresh.ts +37 -57
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +0 -13
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +5 -15
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +0 -11
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +3 -7
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +12 -24
- package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +1 -4
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +1 -2
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +3 -4
- package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +9 -92
- package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +1 -4
- package/runtime/node_modules/@metro-labs/daemon/src/voice/scribe.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/voice/shared.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/voice/speech.ts +0 -4
- package/runtime/node_modules/@metro-labs/daemon/src/voice/store.ts +1 -14
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +0 -1
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
- package/dist/background.js +0 -141
- package/dist/claude.js +0 -204
- package/dist/mcp-config.js +0 -27
- package/dist/native-recovery-scan.js +0 -246
- package/dist/native-recovery-state.js +0 -154
- package/dist/native-recovery.js +0 -164
- package/dist/onboarding.js +0 -44
- package/dist/voice.js +0 -56
- package/runtime/node_modules/@metro-labs/daemon/src/claude/channel-check.ts +0 -56
- package/runtime/node_modules/@metro-labs/daemon/src/claude/pinned.ts +0 -16
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-cleanup.ts +0 -121
- package/runtime/node_modules/@metro-labs/daemon/src/claude/runner.ts +0 -45
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-continuity.ts +0 -57
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +0 -76
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +0 -219
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +0 -119
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +0 -160
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +0 -46
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +0 -150
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +0 -139
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +0 -236
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +0 -208
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +0 -157
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +0 -83
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter-login.ts +0 -192
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter-signin.ts +0 -64
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +0 -36
- package/runtime/node_modules/@metro-labs/daemon/src/voice/approvals.ts +0 -38
- package/runtime/node_modules/@metro-labs/daemon/src/voice/brain.ts +0 -146
- package/runtime/node_modules/@metro-labs/daemon/src/voice/speakable.ts +0 -54
- package/runtime/node_modules/@metro-labs/daemon/src/voice/talk.ts +0 -264
|
@@ -4,6 +4,7 @@ import { errMsg, log } from '@metro-labs/core/log';
|
|
|
4
4
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
5
5
|
import { agentsDir } from '../agents/files.js';
|
|
6
6
|
import { MCP_TOKEN_FILE } from '../mcp-server/token.js';
|
|
7
|
+
import { effectiveSetup, modelViewText } from '../gateway/effective.js';
|
|
7
8
|
import { removeHome, writeHomeText } from './home-fs.js';
|
|
8
9
|
import { closeMetroHome } from './metro-home.js';
|
|
9
10
|
import { agentUser, agentViewDir, type AgentUser } from './user.js';
|
|
@@ -39,17 +40,10 @@ function agentFile(dir: string): string | null {
|
|
|
39
40
|
return json({ version: 1, id: file.id, key: file.key });
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
function
|
|
43
|
-
const cfg = readRecord(join(dir, 'model.json'));
|
|
44
|
-
if (cfg === null || !Array.isArray(cfg.connections)) return null;
|
|
45
|
-
const connections = cfg.connections.filter(isRecord).map((c) => ({ id: c.id, provider: c.provider, model: c.model }));
|
|
46
|
-
return json({ version: 2, route: cfg.route, connections });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function viewFiles(dir = agentsDir()): Map<string, string | null> {
|
|
43
|
+
export function viewFiles(dir = agentsDir(), model: () => string | null = () => modelViewText(effectiveSetup(dir))): Map<string, string | null> {
|
|
50
44
|
const files = new Map<string, string | null>([
|
|
51
45
|
['agent.json', agentFile(dir)],
|
|
52
|
-
['model.json',
|
|
46
|
+
['model.json', model()],
|
|
53
47
|
]);
|
|
54
48
|
for (const name of COPIED) files.set(name, existsSync(join(dir, name)) ? readText(join(dir, name)) : null);
|
|
55
49
|
for (const name of NEVER_COPIED) files.set(name, null);
|
|
@@ -26,9 +26,8 @@ import { refreshConnectorTools, watchConnectorTools } from '../mcp-server/connec
|
|
|
26
26
|
import { setAnnouncedFile } from '../mcp-server/announced.js';
|
|
27
27
|
import { holdServerApproval } from '../mcp/server-approvals.js';
|
|
28
28
|
import { removeConnectorLeftovers } from '../connectors/leftovers.js';
|
|
29
|
-
import { removeMetroPlugin } from '../claude/plugin-cleanup.js';
|
|
30
29
|
import { runningAsRoot } from '../metro-user/privilege.js';
|
|
31
|
-
import { provisionAgentUser } from '../agent-user/provision.js';
|
|
30
|
+
import { prepareAgentRunner, provisionAgentUser } from '../agent-user/provision.js';
|
|
32
31
|
import { applyVault } from '../vault/index.js';
|
|
33
32
|
import { blockImds } from '../metro-user/imds.js';
|
|
34
33
|
import { followRelease } from '../metro-user/root-follow.js';
|
|
@@ -61,6 +60,11 @@ import { startUploadReaper } from '../files/upload-store.js';
|
|
|
61
60
|
import { startAttachReaper } from '../files/attach-reaper.js';
|
|
62
61
|
import { startCloudWatchPublisher } from '../server/cloudwatch.js';
|
|
63
62
|
import { watchFallbackUsage } from '../gateway/usage-refresh.js';
|
|
63
|
+
import { startOrgModels, stopOrgModels } from '../gateway/org-models.js';
|
|
64
|
+
import { effectiveSetup, watchModelChanges } from '../gateway/effective.js';
|
|
65
|
+
import { readClaudeLogins } from '../gateway/claude-store.js';
|
|
66
|
+
import { readAgentKey } from '../connectors/agent-key.js';
|
|
67
|
+
import { METRO_API } from '../connectors/metro-api.js';
|
|
64
68
|
import { isCallEvent, leaveCallsForShutdown, onCallEvent, sweepLeftoverCalls } from '../voice/calls.js';
|
|
65
69
|
|
|
66
70
|
installCrashGuard();
|
|
@@ -115,7 +119,7 @@ async function syncStations(station: StationName): Promise<void> {
|
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
function sessionApis(setLiveEvents: (on: boolean) => void
|
|
122
|
+
function sessionApis(setLiveEvents: (on: boolean) => void): SessionApis {
|
|
119
123
|
return localSessionApis({
|
|
120
124
|
syncStations,
|
|
121
125
|
reloadAgents: async () => {
|
|
@@ -134,7 +138,6 @@ function sessionApis(setLiveEvents: (on: boolean) => void, switchModel: (model:
|
|
|
134
138
|
toolGroups: stationToolGroups,
|
|
135
139
|
prepareAccount,
|
|
136
140
|
liveEvents: setLiveEvents,
|
|
137
|
-
switchModel,
|
|
138
141
|
});
|
|
139
142
|
}
|
|
140
143
|
|
|
@@ -151,10 +154,12 @@ installBearerSessions(agentsDir(), localOwner);
|
|
|
151
154
|
syncGmailHost();
|
|
152
155
|
supervisor.start();
|
|
153
156
|
sweepLeftoverCalls();
|
|
157
|
+
const models = startOrgModels({ owner: localOwner, key: () => readAgentKey(), api: METRO_API, dir: agentsDir });
|
|
154
158
|
const metroMcp = await createMetroMcp({ liveEvents: liveEvents() });
|
|
159
|
+
watchModelChanges(models, metroMcp.switchModel);
|
|
155
160
|
webhookServer = await startWebhookServer(
|
|
156
161
|
emit,
|
|
157
|
-
sessionApis(metroMcp.setLiveEvents
|
|
162
|
+
sessionApis(metroMcp.setLiveEvents),
|
|
158
163
|
metroMcp.httpHandler,
|
|
159
164
|
true,
|
|
160
165
|
);
|
|
@@ -175,7 +180,7 @@ installBearerSessions(agentsDir(), localOwner);
|
|
|
175
180
|
});
|
|
176
181
|
watchConnectorTools();
|
|
177
182
|
startCloudWatchPublisher();
|
|
178
|
-
watchFallbackUsage();
|
|
183
|
+
watchFallbackUsage(() => effectiveSetup(), () => readClaudeLogins());
|
|
179
184
|
announceLocalEndpoint();
|
|
180
185
|
tunnel?.start();
|
|
181
186
|
log.info(
|
|
@@ -193,7 +198,7 @@ async function startClaude(): Promise<void> {
|
|
|
193
198
|
log.error('claude: Metro runs as root, so Claude Code never starts here; reinstall Metro as the metro user');
|
|
194
199
|
return;
|
|
195
200
|
}
|
|
196
|
-
const agentUser =
|
|
201
|
+
const agentUser = provisionAgentUser();
|
|
197
202
|
if (agentUser !== 'off') log.info({ agentUser }, 'agent-user: Claude Code runs as its own user');
|
|
198
203
|
if (agentUser === 'failed') return;
|
|
199
204
|
if (agentUser === 'ready') {
|
|
@@ -202,14 +207,10 @@ async function startClaude(): Promise<void> {
|
|
|
202
207
|
const vault = (await applyVault()).status;
|
|
203
208
|
if (vault.enabled) log.info({ running: vault.running, problem: vault.problem }, 'vault: the agent reaches the internet through the vault');
|
|
204
209
|
}
|
|
205
|
-
const plugin = await removeMetroPlugin().catch((err: unknown) => {
|
|
206
|
-
log.warn({ err: errMsg(err) }, 'claude-plugin: could not check for the old metro plugin');
|
|
207
|
-
return 'failed';
|
|
208
|
-
});
|
|
209
|
-
if (plugin !== 'done') log.info({ outcome: plugin }, 'claude-plugin: checked for the old metro Claude Code plugin');
|
|
210
210
|
tryClaudeSetup();
|
|
211
211
|
try { githubSkills().watch(); } catch { log.warn('github-skills: saved source could not be loaded'); }
|
|
212
212
|
tryMemoryJob(memoryRoutine());
|
|
213
|
+
if (agentUser === 'ready') await prepareAgentRunner();
|
|
213
214
|
watchSession();
|
|
214
215
|
}
|
|
215
216
|
|
|
@@ -226,6 +227,7 @@ async function shutdown(): Promise<void> {
|
|
|
226
227
|
log.warn({ err: errMsg(err) }, 'mcp-server: could not stop the approval relay');
|
|
227
228
|
});
|
|
228
229
|
const leaving = leaveCallsForShutdown();
|
|
230
|
+
stopOrgModels();
|
|
229
231
|
unwatchSession();
|
|
230
232
|
tunnel?.stop();
|
|
231
233
|
if (webhookServer) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
|
-
import { apiFailure, apiSession, requireAdmin, cors, readJsonBody, sendJson
|
|
3
|
+
import { apiFailure, apiSession, requireAdmin, cors, readJsonBody, sendJson } from '@metro-labs/http/api-http';
|
|
4
4
|
import { ApiError } from '@metro-labs/http/api-error';
|
|
5
5
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
6
6
|
import { githubSkillsAnswer } from './github-api.js';
|
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
stopSession,
|
|
36
36
|
type SessionDeps,
|
|
37
37
|
} from './session.js';
|
|
38
|
-
import { claudeVersion, updateClaude, type VersionDeps } from './version.js';
|
|
39
38
|
import { receiveSessionFile, sessionFilePath } from './session-files.js';
|
|
40
39
|
import { createReadStream } from '../agent-user/agent-fs.js';
|
|
41
40
|
import { pipeline } from 'node:stream/promises';
|
|
@@ -61,7 +60,6 @@ export interface ClaudeApiDeps extends SetupApiDeps {
|
|
|
61
60
|
dir?: () => string;
|
|
62
61
|
login?: LoginDeps;
|
|
63
62
|
claudeLogins?: ClaudeLoginDeps;
|
|
64
|
-
version?: VersionDeps;
|
|
65
63
|
skillSource?: GitHubSkills;
|
|
66
64
|
}
|
|
67
65
|
|
|
@@ -135,11 +133,10 @@ async function created(req: IncomingMessage, path: string, dir: string): Promise
|
|
|
135
133
|
return createClaudeSkill(body.name, typeof body.text === 'string' ? body.text : undefined, dir);
|
|
136
134
|
}
|
|
137
135
|
|
|
138
|
-
const ADMIN_ONLY = /^\/api\/claude\/(login|session|
|
|
136
|
+
const ADMIN_ONLY = /^\/api\/claude\/(login|session|setup|skill-source)(\/|$)/;
|
|
139
137
|
const LOGIN = 'login';
|
|
140
138
|
const SESSION = 'session';
|
|
141
139
|
const SETUP = 'setup';
|
|
142
|
-
const VERSION = 'version';
|
|
143
140
|
|
|
144
141
|
function sessionCommand(body: Record<string, unknown>, session: SessionDeps): unknown {
|
|
145
142
|
if (body.action === 'start') {
|
|
@@ -209,18 +206,9 @@ function answer(method: string, path: string, query: URLSearchParams, dir: strin
|
|
|
209
206
|
return handler(query, dir, item);
|
|
210
207
|
}
|
|
211
208
|
|
|
212
|
-
|
|
213
|
-
const version: VersionDeps = { ...deps.version, session: deps.version?.session ?? deps.session };
|
|
214
|
-
const method = req.method ?? 'GET';
|
|
215
|
-
if (method === 'GET') return Promise.resolve(claudeVersion(version));
|
|
216
|
-
if (method !== 'POST') throw new ApiError('method not allowed', 405);
|
|
217
|
-
return Promise.resolve(updateClaude(version));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const SINGLETONS: Record<string, (req: IncomingMessage, deps: ClaudeApiDeps, session: ApiSession) => Promise<unknown>> = {
|
|
209
|
+
const SINGLETONS: Record<string, (req: IncomingMessage, deps: ClaudeApiDeps) => Promise<unknown>> = {
|
|
221
210
|
[SESSION]: sessionAnswer,
|
|
222
211
|
[SETUP]: setupAnswer,
|
|
223
|
-
[VERSION]: versionAnswer,
|
|
224
212
|
};
|
|
225
213
|
|
|
226
214
|
function skillsAnswer(req: IncomingMessage, path: string, search: string, dir: string, source: GitHubSkills): unknown {
|
|
@@ -245,14 +233,14 @@ function fileAnswer(req: IncomingMessage, path: string, search: string, dir: str
|
|
|
245
233
|
return answer(req.method ?? 'GET', path, new URLSearchParams(search), dir);
|
|
246
234
|
}
|
|
247
235
|
|
|
248
|
-
function routed(req: IncomingMessage, path: string, search: string, deps: ClaudeApiDeps
|
|
236
|
+
function routed(req: IncomingMessage, path: string, search: string, deps: ClaudeApiDeps): unknown {
|
|
249
237
|
const dir = (deps.dir ?? claudeDir)();
|
|
250
238
|
const [head = '', item = ''] = parts(path);
|
|
251
239
|
if (head === LOGIN) return loginAnswer(req, item, deps);
|
|
252
240
|
if (head === 'skill-source' && parts(path).length === 1) return githubSkillsAnswer(req, deps.skillSource);
|
|
253
241
|
if (head === 'skills') return skillsAnswer(req, path, search, dir, sourceOf(deps));
|
|
254
242
|
const single = item === '' ? SINGLETONS[head] : undefined;
|
|
255
|
-
if (single !== undefined) return single(req, deps
|
|
243
|
+
if (single !== undefined) return single(req, deps);
|
|
256
244
|
return fileAnswer(req, path, search, dir);
|
|
257
245
|
}
|
|
258
246
|
|
|
@@ -303,7 +291,7 @@ export function handleClaudeRequest(
|
|
|
303
291
|
}
|
|
304
292
|
if (req.method !== 'GET' && ADMIN_ONLY.test(path.replace(/\/{2,}/g, '/'))) requireAdmin(session);
|
|
305
293
|
if (await streamed(req, res, path, search, (deps.dir ?? claudeDir)())) return;
|
|
306
|
-
sendJson(req, res, 200, await routed(req, path, search, deps
|
|
294
|
+
sendJson(req, res, 200, await routed(req, path, search, deps));
|
|
307
295
|
})
|
|
308
296
|
.catch((err: unknown) => {
|
|
309
297
|
if (err instanceof ApiError) apiFailure(req, res, err, 'claude-api');
|
|
@@ -10,7 +10,6 @@ import { readFileSync, statSync } from '../agent-user/agent-fs.js';
|
|
|
10
10
|
import { receiveHomeFile, writeHomeText } from '../agent-user/home-fs.js';
|
|
11
11
|
import { agentsDir } from '../agents/files.js';
|
|
12
12
|
import { claudeDir } from './files.js';
|
|
13
|
-
import { readSetupState } from './setup-state.js';
|
|
14
13
|
import { GitHubFailure, GitHubReader } from './github-fetch.js';
|
|
15
14
|
import { githubSource, validatedGeneration } from './github-validate.js';
|
|
16
15
|
import type { ClaudeSkill } from './skills.js';
|
|
@@ -29,7 +28,6 @@ interface Stored {
|
|
|
29
28
|
export interface GitHubSkillsDeps {
|
|
30
29
|
agents: string;
|
|
31
30
|
claude: string;
|
|
32
|
-
sdk(): boolean;
|
|
33
31
|
request?: (url: string, init: RequestInit) => Promise<Response>;
|
|
34
32
|
}
|
|
35
33
|
const EMPTY: Stored = { source: null, prepared: null, etag: null, checkedAt: null, retryAt: 0, problem: null, rateLimit: 0, repositoryId: null };
|
|
@@ -107,7 +105,7 @@ export class GitHubSkills {
|
|
|
107
105
|
view(activation = this.activation(), loaded = this.loaded(activation)): object {
|
|
108
106
|
const { source, prepared, checkedAt, problem } = this.stored;
|
|
109
107
|
return {
|
|
110
|
-
supported:
|
|
108
|
+
supported: true,
|
|
111
109
|
source: source === null ? null : { repository: source.repository, ref: source.ref, folder: source.folder, hasToken: true },
|
|
112
110
|
checking: this.busy, checkedAt, problem: this.unreadable ? 'The saved GitHub source is unreadable. Restore its configuration before changing it. Local skills are still available.' : problem,
|
|
113
111
|
prepared: prepared === null ? null : { generation: prepared.id, source: prepared.source, skillCount: prepared.skills.length },
|
|
@@ -142,7 +140,6 @@ export class GitHubSkills {
|
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
async configure(raw: unknown): Promise<object> {
|
|
145
|
-
if (!this.deps.sdk()) throw new ApiError('GitHub skills require the Agent SDK runner. Local skills are unchanged.', 409);
|
|
146
143
|
if (this.busy) throw new ApiError('A skills sync is already running. Try again when it finishes.', 409);
|
|
147
144
|
const source = githubSource(raw);
|
|
148
145
|
this.readable();
|
|
@@ -193,11 +190,10 @@ export class GitHubSkills {
|
|
|
193
190
|
}
|
|
194
191
|
|
|
195
192
|
async sync(force = false): Promise<void> {
|
|
196
|
-
if (force && !this.deps.sdk()) throw new ApiError('GitHub skills require the Agent SDK runner. Local skills are unchanged.', 409);
|
|
197
193
|
this.readable();
|
|
198
194
|
this.offer();
|
|
199
195
|
const source = this.stored.source;
|
|
200
|
-
if (source === null || this.busy ||
|
|
196
|
+
if (source === null || this.busy || this.waiting(force)) return;
|
|
201
197
|
const epoch = this.epoch;
|
|
202
198
|
this.busy = true;
|
|
203
199
|
try { await this.fetch(source, epoch, force); }
|
|
@@ -254,6 +250,6 @@ export class GitHubSkills {
|
|
|
254
250
|
|
|
255
251
|
let current: GitHubSkills | null = null;
|
|
256
252
|
export function githubSkills(): GitHubSkills {
|
|
257
|
-
current ??= new GitHubSkills({ agents: agentsDir(), claude: claudeDir()
|
|
253
|
+
current ??= new GitHubSkills({ agents: agentsDir(), claude: claudeDir() });
|
|
258
254
|
return current;
|
|
259
255
|
}
|
|
@@ -4,7 +4,7 @@ import { markOnboardingDone } from './onboarding.js';
|
|
|
4
4
|
import { ApiError } from '@metro-labs/http/api-error';
|
|
5
5
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
6
6
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
7
|
-
import { agentCommand, asAgent, claudeBin } from '../agent-user/user.js';
|
|
7
|
+
import { agentCommand, asAgent, CLAUDE_ENV, claudeBin } from '../agent-user/user.js';
|
|
8
8
|
|
|
9
9
|
const LOGIN_ARGS = ['auth', 'login', '--claudeai'];
|
|
10
10
|
const STATUS_COMMAND = ['auth', 'status', '--json'];
|
|
@@ -95,14 +95,14 @@ function remembered<T>(slot: Checked<T> | undefined, read: () => T, keep: (c: Ch
|
|
|
95
95
|
return value;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
function forgetClaudeChecks(): void {
|
|
98
|
+
export function forgetClaudeChecks(): void {
|
|
99
99
|
delete checks.installed;
|
|
100
100
|
delete checks.account;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export function claudeInstalled(): boolean {
|
|
104
104
|
return remembered(checks.installed, () => {
|
|
105
|
-
const run = spawnSync(...asAgent(claudeBin(), ['--version']), { stdio: 'ignore' });
|
|
105
|
+
const run = spawnSync(...asAgent(claudeBin(), ['--version'], CLAUDE_ENV), { stdio: 'ignore', env: { ...process.env, ...CLAUDE_ENV } });
|
|
106
106
|
return run.error === undefined && run.status === 0;
|
|
107
107
|
}, (c) => {
|
|
108
108
|
checks.installed = c;
|
|
@@ -125,7 +125,8 @@ export function claudeAccount(): ClaudeAccount {
|
|
|
125
125
|
|
|
126
126
|
const textOf = (value: unknown): string | null => (typeof value === 'string' && value !== '' ? value : null);
|
|
127
127
|
|
|
128
|
-
export function claudeAccountIn(
|
|
128
|
+
export function claudeAccountIn(extra: Record<string, string>): ClaudeAccount {
|
|
129
|
+
const env = { ...CLAUDE_ENV, ...extra };
|
|
129
130
|
const run = spawnSync(...asAgent(claudeBin(), STATUS_COMMAND, env), { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: { ...process.env, ...env } });
|
|
130
131
|
if (run.error !== undefined || typeof run.stdout !== 'string') return SIGNED_OUT;
|
|
131
132
|
try {
|
|
@@ -165,7 +166,7 @@ export function startClaudeLogin(deps: LoginDeps = {}, now = Date.now(), target:
|
|
|
165
166
|
sweep(now);
|
|
166
167
|
const running = runningFor(target);
|
|
167
168
|
if (running !== null) return view(running);
|
|
168
|
-
const env = { TERM: 'xterm-256color', ...target?.env };
|
|
169
|
+
const env = { TERM: 'xterm-256color', ...CLAUDE_ENV, ...target?.env };
|
|
169
170
|
const command = deps.command ?? agentCommand([claudeBin(), ...LOGIN_ARGS], env);
|
|
170
171
|
const session: Live = {
|
|
171
172
|
id: randomBytes(9).toString('base64url'),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync
|
|
1
|
+
import { existsSync, readFileSync } from '../agent-user/agent-fs.js';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
@@ -36,25 +36,3 @@ export function markOnboardingDone(path = claudeConfigPath()): OnboardingMark {
|
|
|
36
36
|
writeHomeText(path, `${JSON.stringify({ ...config, [FLAG]: true }, null, 2)}\n`, 0o600);
|
|
37
37
|
return 'marked';
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
const TRUST = 'hasTrustDialogAccepted';
|
|
41
|
-
|
|
42
|
-
function realDir(dir: string): string {
|
|
43
|
-
try {
|
|
44
|
-
return realpathSync(dir);
|
|
45
|
-
} catch {
|
|
46
|
-
return dir;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function trustFolder(dir: string, path = claudeConfigPath()): OnboardingMark {
|
|
51
|
-
const config = readConfig(path);
|
|
52
|
-
if (config === null) return 'unreadable';
|
|
53
|
-
const key = realDir(dir);
|
|
54
|
-
const projects = isRecord(config.projects) ? config.projects : {};
|
|
55
|
-
const project = isRecord(projects[key]) ? projects[key] : {};
|
|
56
|
-
if (project[TRUST] === true) return 'already';
|
|
57
|
-
const next = { ...config, projects: { ...projects, [key]: { ...project, [TRUST]: true } } };
|
|
58
|
-
writeHomeText(path, `${JSON.stringify(next, null, 2)}\n`, 0o600);
|
|
59
|
-
return 'marked';
|
|
60
|
-
}
|
|
@@ -3,7 +3,6 @@ import { ApiError } from '@metro-labs/http/api-error';
|
|
|
3
3
|
import type { RunnerActivity } from '@metro-labs/core/runner-activity';
|
|
4
4
|
import { readAgentActivity } from './runner-activity.js';
|
|
5
5
|
import { sdkAlive, stopSdkRunner } from './runner-process.js';
|
|
6
|
-
import { continueArgs, sdkPending } from './session-continuity.js';
|
|
7
6
|
import { syncAgentView } from '../agent-user/view.js';
|
|
8
7
|
import { homedir } from 'node:os';
|
|
9
8
|
import { join } from 'node:path';
|
|
@@ -12,13 +11,10 @@ import { isRecord } from '@metro-labs/core/is-record';
|
|
|
12
11
|
import { readJson, writeJson } from '@metro-labs/core/secure-fs';
|
|
13
12
|
import { METRO_VERSION } from '@metro-labs/core/version';
|
|
14
13
|
import { agentsDir, listAgentFiles } from '../agents/files.js';
|
|
15
|
-
import { notReady,
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { claudeAccount, claudeInstalled } from './login.js';
|
|
19
|
-
import { trustFolder } from './onboarding.js';
|
|
14
|
+
import { notReady, primaryRoute, type ModelSetup } from '../gateway/model-config.js';
|
|
15
|
+
import { effectiveSetup } from '../gateway/effective.js';
|
|
16
|
+
import { claudeAccount, claudeInstalled, forgetClaudeChecks } from './login.js';
|
|
20
17
|
import { inSessionScope } from './memory.js';
|
|
21
|
-
import { channelState, forgetChannelState, noteChannelState, type ChannelState } from './channel-check.js';
|
|
22
18
|
import { runningAsRoot } from '../metro-user/privilege.js';
|
|
23
19
|
import { agentExtraEnv, agentUser, agentUserExpected, agentViewDir, asAgent, claudeHome } from '../agent-user/user.js';
|
|
24
20
|
|
|
@@ -31,41 +27,35 @@ function sessionEnv(): Record<string, string> {
|
|
|
31
27
|
|
|
32
28
|
const SESSION_NAME = 'metro';
|
|
33
29
|
const STATE_FILE = 'claude-session.json';
|
|
34
|
-
const WARNING = 'WARNING: Loading development channels';
|
|
35
|
-
const CONFIRM_POLL_MS = 500;
|
|
36
|
-
const CONFIRM_WAIT_MS = 60_000;
|
|
37
30
|
const WATCH_MS = 15_000;
|
|
38
31
|
const STRIKE_WINDOW_MS = 60_000;
|
|
39
32
|
const MAX_STRIKES = 5;
|
|
40
33
|
const PAUSE_MS = 30 * 60_000;
|
|
34
|
+
const PREPARE_MS = 16 * 60_000;
|
|
41
35
|
|
|
42
36
|
export interface SessionDeps {
|
|
43
37
|
tmux?: string;
|
|
44
38
|
metro?: string[];
|
|
45
|
-
runner?: string[];
|
|
46
39
|
home?: string;
|
|
47
40
|
agents?: string;
|
|
48
41
|
signedIn?: () => boolean;
|
|
42
|
+
model?: () => ModelSetup;
|
|
49
43
|
now?: () => number;
|
|
50
44
|
version?: string;
|
|
51
|
-
prepare?: (runner: HarnessRunner) => Promise<void>;
|
|
52
45
|
}
|
|
53
46
|
|
|
54
47
|
export interface SessionStatus {
|
|
55
48
|
name: string;
|
|
56
49
|
running: boolean;
|
|
57
|
-
runner?: HarnessRunner;
|
|
58
50
|
activity?: RunnerActivity | null;
|
|
59
51
|
autostart: boolean;
|
|
60
52
|
blocked: string | null;
|
|
61
53
|
lastStartedAt: string | null;
|
|
62
54
|
lastError: string | null;
|
|
63
|
-
channels?: ChannelState | null;
|
|
64
55
|
}
|
|
65
56
|
|
|
66
57
|
export interface SessionSnapshot {
|
|
67
58
|
running: boolean;
|
|
68
|
-
runner: HarnessRunner | null;
|
|
69
59
|
activity: RunnerActivity | null;
|
|
70
60
|
lastStartedAt: string | null;
|
|
71
61
|
}
|
|
@@ -120,64 +110,42 @@ export function metroCli(args: string[], fixed?: string[]): string[] {
|
|
|
120
110
|
|
|
121
111
|
const sessionHome = (deps: SessionDeps): string => deps.home ?? claudeHome() ?? homedir();
|
|
122
112
|
|
|
123
|
-
export function sessionRunner(deps: SessionDeps = {}): HarnessRunner {
|
|
124
|
-
const agents = deps.agents ?? agentsDir();
|
|
125
|
-
const runner = readState(agents).runner;
|
|
126
|
-
return isHarnessRunner(runner) ? runner : harnessRunner(agents);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
113
|
export function assertSessionStopped(deps: SessionDeps = {}): void {
|
|
130
|
-
if (sessionLive(deps)) throw new ApiError('The session is running. Let it finish its work, stop it, then change its
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function assertSdkDrained(deps: SessionDeps = {}): void {
|
|
134
|
-
if (sdkPending(sessionHome(deps)) !== 0)
|
|
135
|
-
throw new ApiError('The Agent SDK still has chat messages it has not answered, or its saved state cannot be read. Start it so it answers them (fix the Model page first if it cannot start), stop it, then switch runners.', 409);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
let changing = false;
|
|
139
|
-
|
|
140
|
-
export async function withSessionChange<T>(run: () => Promise<T>): Promise<T> {
|
|
141
|
-
if (changing) throw new ApiError('A session change is already being prepared. Try again when it finishes.', 409);
|
|
142
|
-
changing = true;
|
|
143
|
-
try {
|
|
144
|
-
return await run();
|
|
145
|
-
} finally {
|
|
146
|
-
changing = false;
|
|
147
|
-
}
|
|
114
|
+
if (sessionLive(deps)) throw new ApiError('The session is running. Let it finish its work, stop it, then change its permissions or prompt and start it again.', 409);
|
|
148
115
|
}
|
|
149
116
|
|
|
150
|
-
export
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
117
|
+
export function prepareRunnerStore(): Promise<void> {
|
|
118
|
+
const [bin = 'metro', ...args] = metroCli(['agent', '--prepare']);
|
|
119
|
+
return new Promise<void>((resolve, reject) => {
|
|
120
|
+
const child = spawn(...asAgent(bin, args), { stdio: ['ignore', 'ignore', 'pipe'], cwd: '/', timeout: PREPARE_MS });
|
|
121
|
+
let said = '';
|
|
122
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
123
|
+
said = (said + chunk.toString('utf8')).slice(-2000);
|
|
124
|
+
});
|
|
125
|
+
child.on('error', (err) => {
|
|
126
|
+
forgetClaudeChecks();
|
|
127
|
+
reject(err);
|
|
128
|
+
});
|
|
158
129
|
child.on('close', (code) => {
|
|
130
|
+
forgetClaudeChecks();
|
|
159
131
|
if (code === 0) resolve();
|
|
160
|
-
else reject(new
|
|
132
|
+
else reject(new Error(`metro agent --prepare failed: ${said.trim() || `exit ${String(code)}`}`));
|
|
161
133
|
});
|
|
162
134
|
});
|
|
163
135
|
}
|
|
164
136
|
|
|
165
|
-
|
|
166
|
-
if (harnessRunner(deps.agents ?? agentsDir()) === 'sdk') return metroCli(['agent'], deps.runner);
|
|
167
|
-
return [...metroCli(['claude'], deps.metro), ...continueArgs(home, claudeDir(), deps.agents)];
|
|
168
|
-
}
|
|
137
|
+
const loginNotRuledOut = (): boolean => claudeAccount().signedIn || !claudeInstalled();
|
|
169
138
|
|
|
170
139
|
function credentialReady(deps: SessionDeps): string | null {
|
|
171
|
-
const signedIn = deps.signedIn ??
|
|
140
|
+
const signedIn = deps.signedIn ?? loginNotRuledOut;
|
|
172
141
|
if (signedIn()) return null;
|
|
173
142
|
try {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
if (conn !== null && notReady(cfg) === null && (conn.provider !== 'anthropic' || conn.apiKey !== '')) return null;
|
|
143
|
+
const primary = primaryRoute((deps.model ?? (() => effectiveSetup(deps.agents ?? agentsDir())))());
|
|
144
|
+
if (primary !== null && notReady(primary) === null && (primary.connection.provider !== 'anthropic' || primary.connection.apiKey !== '')) return null;
|
|
177
145
|
} catch (err) {
|
|
178
|
-
return `the
|
|
146
|
+
return `the model setup is not readable (${errMsg(err)})`;
|
|
179
147
|
}
|
|
180
|
-
return 'Claude Code is not signed in and the
|
|
148
|
+
return 'Claude Code is not signed in and the organization routes this agent to no model yet';
|
|
181
149
|
}
|
|
182
150
|
|
|
183
151
|
const agentUserMissing = (deps: SessionDeps): boolean =>
|
|
@@ -187,64 +155,26 @@ const ROOT_REFUSED = 'Metro runs as root; reinstall it as the metro user';
|
|
|
187
155
|
|
|
188
156
|
export function sessionBlocked(deps: SessionDeps = {}): string | null {
|
|
189
157
|
if (runningAsRoot()) return ROOT_REFUSED;
|
|
190
|
-
|
|
191
|
-
if (harnessRunner(agents) === 'sdk' && !sdkAllowed(agents)) return SDK_NEEDS_KEY;
|
|
192
|
-
if (listAgentFiles(agents).length === 0) return 'no agent on this machine yet';
|
|
158
|
+
if (listAgentFiles(deps.agents ?? agentsDir()).length === 0) return 'no agent on this machine yet';
|
|
193
159
|
return launchBlocked(deps);
|
|
194
160
|
}
|
|
195
161
|
|
|
196
162
|
function launchBlocked(deps: SessionDeps): string | null {
|
|
197
163
|
if (agentUserMissing(deps))
|
|
198
164
|
return 'Claude Code runs as the user agent here, and that user is not ready yet; see the log';
|
|
199
|
-
if (deps.metro === undefined && !claudeInstalled()) return 'Claude Code is not installed on this machine';
|
|
200
165
|
if (!tmuxOk(deps.tmux ?? 'tmux', ['-V'])) return 'tmux is not installed on this machine';
|
|
201
166
|
return credentialReady(deps);
|
|
202
167
|
}
|
|
203
168
|
|
|
204
|
-
function
|
|
205
|
-
const run = spawnSync(...asAgent(tmux, ['capture-pane', '-p', '-t', SESSION_NAME]), { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
206
|
-
return run.error === undefined && run.status === 0 ? run.stdout : '';
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function noteLaunchLines(pane: string, seen: Set<string>): void {
|
|
210
|
-
for (const line of pane.split('\n').map((l) => l.trim()))
|
|
211
|
-
if (line.startsWith('metro claude:') && !seen.has(line)) {
|
|
212
|
-
seen.add(line);
|
|
213
|
-
log.info({ line }, 'claude-session: metro claude said');
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function confirmChannels(tmux: string, home: string, startedAt: number): void {
|
|
218
|
-
const until = startedAt + CONFIRM_WAIT_MS;
|
|
219
|
-
const seen = new Set<string>();
|
|
220
|
-
let confirmed = false;
|
|
221
|
-
let checked = false;
|
|
222
|
-
const tick = (): void => {
|
|
223
|
-
const pane = capturePane(tmux);
|
|
224
|
-
noteLaunchLines(pane, seen);
|
|
225
|
-
checked ||= noteChannelState(home, startedAt);
|
|
226
|
-
if (!confirmed && pane.includes(WARNING) && pane.includes('server:metro')) {
|
|
227
|
-
spawnSync(...asAgent(tmux, ['send-keys', '-t', SESSION_NAME, 'Enter']), { stdio: 'ignore' });
|
|
228
|
-
log.info('claude-session: confirmed the development channels dialog for server:metro');
|
|
229
|
-
confirmed = true;
|
|
230
|
-
}
|
|
231
|
-
if (Date.now() < until && sessionRunning(tmux)) setTimeout(tick, CONFIRM_POLL_MS).unref();
|
|
232
|
-
};
|
|
233
|
-
setTimeout(tick, CONFIRM_POLL_MS).unref();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function recordStart(deps: SessionDeps, tmux: string, now: number, run: { error?: Error; status: number | null; stderr: string }): void {
|
|
169
|
+
function recordStart(deps: SessionDeps, now: number, run: { error?: Error; status: number | null; stderr: string }): void {
|
|
237
170
|
memory.lastStartedAt = now;
|
|
238
|
-
forgetChannelState();
|
|
239
171
|
if (run.error !== undefined || run.status !== 0) {
|
|
240
172
|
memory.lastError = run.error === undefined ? `tmux new-session exited ${String(run.status)}: ${run.stderr.trim()}` : errMsg(run.error);
|
|
241
173
|
log.warn({ err: memory.lastError }, 'claude-session: tmux could not start');
|
|
242
174
|
return;
|
|
243
175
|
}
|
|
244
176
|
memory.lastError = null;
|
|
245
|
-
|
|
246
|
-
writeState(agents, { version: deps.version ?? METRO_VERSION, runner: harnessRunner(agents) });
|
|
247
|
-
confirmChannels(tmux, sessionHome(deps), now);
|
|
177
|
+
writeState(deps.agents ?? agentsDir(), { version: deps.version ?? METRO_VERSION });
|
|
248
178
|
}
|
|
249
179
|
|
|
250
180
|
function tmuxLaunch(deps: SessionDeps, tmux: string, args: string[]): [string, string[]] {
|
|
@@ -253,7 +183,6 @@ function tmuxLaunch(deps: SessionDeps, tmux: string, args: string[]): [string, s
|
|
|
253
183
|
}
|
|
254
184
|
|
|
255
185
|
export function startSession(deps: SessionDeps = {}): SessionStatus {
|
|
256
|
-
if (changing) throw new ApiError('A runner change is being prepared. Start the session after it finishes.', 409);
|
|
257
186
|
const status = sessionStatus(deps);
|
|
258
187
|
if (status.running) return status;
|
|
259
188
|
if (status.blocked !== null) throw new ApiError(`Cannot start the session: ${status.blocked}`, 409);
|
|
@@ -261,13 +190,12 @@ export function startSession(deps: SessionDeps = {}): SessionStatus {
|
|
|
261
190
|
const tmux = deps.tmux ?? 'tmux';
|
|
262
191
|
const home = sessionHome(deps);
|
|
263
192
|
const now = (deps.now ?? Date.now)();
|
|
264
|
-
const
|
|
265
|
-
const [command = 'metro', ...args] = metroCommand(deps, home);
|
|
193
|
+
const [command = 'metro', ...args] = metroCli(['agent'], deps.metro);
|
|
266
194
|
const tmuxArgs = ['new-session', '-d', '-s', SESSION_NAME, '-c', home, '-x', '200', '-y', '50', command, ...args];
|
|
267
195
|
const launch = tmuxLaunch(deps, tmux, tmuxArgs);
|
|
268
196
|
const run = spawnSync(...launch, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], cwd: '/' });
|
|
269
|
-
recordStart(deps,
|
|
270
|
-
if (memory.lastError === null) log.info({ home,
|
|
197
|
+
recordStart(deps, now, run);
|
|
198
|
+
if (memory.lastError === null) log.info({ home, command: [command, ...args].join(' ') }, 'claude-session: started the Agent SDK runner in tmux');
|
|
271
199
|
return sessionStatus(deps);
|
|
272
200
|
}
|
|
273
201
|
|
|
@@ -277,32 +205,26 @@ export function stopSession(deps: SessionDeps = {}, cancelActive = false): Sessi
|
|
|
277
205
|
return sessionStatus(deps);
|
|
278
206
|
}
|
|
279
207
|
|
|
280
|
-
export const
|
|
281
|
-
|
|
208
|
+
export const runnerAlive = (deps: SessionDeps = {}): boolean => sdkAlive(readAgentActivity(sessionHome(deps)));
|
|
209
|
+
|
|
210
|
+
export const sessionLive = (deps: SessionDeps = {}): boolean => sessionRunning(deps.tmux ?? 'tmux') || runnerAlive(deps);
|
|
282
211
|
|
|
283
212
|
export function sessionSnapshot(deps: SessionDeps = {}): SessionSnapshot {
|
|
284
213
|
const activity = readAgentActivity(sessionHome(deps));
|
|
285
|
-
const sdk = sdkAlive(activity);
|
|
286
|
-
const tmux = sessionRunning(deps.tmux ?? 'tmux');
|
|
287
|
-
const runner = sdk ? 'sdk' : tmux ? readState(deps.agents ?? agentsDir()).runner : null;
|
|
288
214
|
return {
|
|
289
|
-
running:
|
|
290
|
-
runner: isHarnessRunner(runner) ? runner : null,
|
|
215
|
+
running: sdkAlive(activity) || sessionRunning(deps.tmux ?? 'tmux'),
|
|
291
216
|
activity,
|
|
292
217
|
lastStartedAt: memory.lastStartedAt === null ? null : new Date(memory.lastStartedAt).toISOString(),
|
|
293
218
|
};
|
|
294
219
|
}
|
|
295
220
|
|
|
296
221
|
export function sessionStatus(deps: SessionDeps = {}): SessionStatus {
|
|
297
|
-
const snapshot = sessionSnapshot(deps);
|
|
298
222
|
return {
|
|
299
223
|
name: SESSION_NAME,
|
|
300
|
-
...
|
|
301
|
-
runner: snapshot.runner ?? sessionRunner(deps),
|
|
224
|
+
...sessionSnapshot(deps),
|
|
302
225
|
autostart: autostartEnabled(deps.agents ?? agentsDir()),
|
|
303
226
|
blocked: sessionBlocked(deps),
|
|
304
227
|
lastError: memory.lastError,
|
|
305
|
-
channels: channelState(),
|
|
306
228
|
};
|
|
307
229
|
}
|
|
308
230
|
|
|
@@ -312,7 +234,7 @@ function strike(now: number): boolean {
|
|
|
312
234
|
if (memory.strikes < MAX_STRIKES) return false;
|
|
313
235
|
memory.pausedUntil = now + PAUSE_MS;
|
|
314
236
|
memory.strikes = 0;
|
|
315
|
-
log.warn({ pauseMinutes: PAUSE_MS / 60_000 }, 'claude-session:
|
|
237
|
+
log.warn({ pauseMinutes: PAUSE_MS / 60_000 }, 'claude-session: the session keeps exiting right after starting; not restarting it for a while');
|
|
316
238
|
return true;
|
|
317
239
|
}
|
|
318
240
|
|
|
@@ -346,7 +268,6 @@ function situation(r: Resolved): 'running' | 'stale' | 'absent' {
|
|
|
346
268
|
export function ensureSession(deps: SessionDeps = {}): Ensured {
|
|
347
269
|
const r = resolved(deps);
|
|
348
270
|
if (!autostartEnabled(r.agents)) return 'off';
|
|
349
|
-
if (changing) return 'blocked';
|
|
350
271
|
const found = situation(r);
|
|
351
272
|
if (found === 'running' || (found === 'absent' && sessionLive(deps))) return 'running';
|
|
352
273
|
const held = holdsBack(deps, r.now);
|
|
@@ -366,7 +287,7 @@ export function watchSession(deps: SessionDeps = {}, everyMs = WATCH_MS): void {
|
|
|
366
287
|
const tick = (): void => {
|
|
367
288
|
try {
|
|
368
289
|
const outcome = ensureSession(deps);
|
|
369
|
-
if (outcome === 'started' || outcome === 'restarted') log.info({ outcome }, 'claude-session:
|
|
290
|
+
if (outcome === 'started' || outcome === 'restarted') log.info({ outcome }, 'claude-session: the Agent SDK runner is up');
|
|
370
291
|
} catch (err) {
|
|
371
292
|
log.warn({ err: errMsg(err) }, 'claude-session: check failed');
|
|
372
293
|
}
|