@withtruing/companion 0.13.3 → 0.14.1
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/README.md +24 -25
- package/dist/anthropic-output-schema.js +41 -0
- package/dist/api-key-caller.js +9 -2
- package/dist/claude-cli-caller.js +107 -8
- package/dist/cli.js +833 -206
- package/dist/codex-cli-caller.js +85 -14
- package/dist/config.js +177 -2
- package/dist/git-range.js +88 -46
- package/dist/http-client.js +107 -10
- package/dist/investigation-reader.js +12 -6
- package/dist/managed-repo.js +132 -0
- package/dist/observed-sources.js +132 -6
- package/dist/reader-result-contract.js +296 -2
- package/dist/repository-provider.js +608 -0
- package/dist/schema-entities.js +18 -18
- package/dist/service.js +7 -2
- package/dist/surface-mapper.js +53 -11
- package/dist/text-model.js +91 -8
- package/dist/whole-read/archive.js +196 -0
- package/dist/whole-read/checks.js +297 -0
- package/dist/whole-read/claims.js +196 -0
- package/dist/whole-read/coverage-ledger.js +51 -0
- package/dist/whole-read/decode.js +234 -0
- package/dist/whole-read/drafting.js +502 -0
- package/dist/whole-read/generated-rules.js +68 -0
- package/dist/whole-read/go-proof.js +255 -0
- package/dist/whole-read/grouping-prompt.js +19 -0
- package/dist/whole-read/grouping.js +247 -0
- package/dist/whole-read/identity.js +103 -0
- package/dist/whole-read/import-graph.js +293 -0
- package/dist/whole-read/inventory-collector.js +55 -0
- package/dist/whole-read/inventory-rules.js +141 -0
- package/dist/whole-read/lockfiles.js +947 -0
- package/dist/whole-read/notes.js +216 -0
- package/dist/whole-read/npm-proof.js +272 -0
- package/dist/whole-read/owner-decision-detectors.js +270 -0
- package/dist/whole-read/packing.js +612 -0
- package/dist/whole-read/pages.js +429 -0
- package/dist/whole-read/phase-policy.js +54 -0
- package/dist/whole-read/prd-build.js +279 -0
- package/dist/whole-read/prd-fidelity.js +14 -0
- package/dist/whole-read/python-proof.js +489 -0
- package/dist/whole-read/read-prompt.js +67 -0
- package/dist/whole-read/redaction.js +411 -0
- package/dist/whole-read/relationship-mapping.js +98 -0
- package/dist/whole-read/sanitize.js +266 -0
- package/dist/whole-read/secret-rules.js +26 -0
- package/dist/whole-read/structure.js +300 -0
- package/dist/whole-read/types.js +2 -0
- package/dist/whole-read-claims.js +12 -0
- package/dist/whole-read-git.js +340 -0
- package/dist/whole-read-pool.js +80 -0
- package/dist/whole-read-runner.js +945 -0
- package/dist/whole-read-shared-source.js +194 -0
- package/dist/whole-read-temp.js +479 -0
- package/package.json +8 -2
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import Anthropic from '@anthropic-ai/sdk';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { addConnection, configPath, enableWorkspacePolling, loadConfig, managedReposDir, normalizeConsoleUrl, recordWorkspaceId, removePairing, } from './config.js';
|
|
7
|
-
import { CompanionClient, CompanionHttpError, PairingHttpError, } from './http-client.js';
|
|
8
|
-
import { assertIsRepo,
|
|
6
|
+
import { addConnection, cleanupMigratedManagedRepositories, configPath, enableWorkspacePolling, hasTemporaryManagedRepository, loadConfig, managedReposDir, migrateManagedRepositoriesToTemporaryStorage, normalizeConsoleUrl, recordWorkspaceId, removePairing, } from './config.js';
|
|
7
|
+
import { CompanionClient, CompanionHttpError, CLAUDE_CLI_FAST_MODEL, wholeReadBackendName as sharedWholeReadBackendName, CODEX_FAST_MODEL, PairingHttpError, } from './http-client.js';
|
|
8
|
+
import { assertIsRepo, currentCommit, GIT_UNAVAILABLE_MESSAGE, REMOTE_TROUBLESHOOTING_HINT, repositoryIdentity, RepositoryIdentityError, repositoryTopLevel, } from './local-scan.js';
|
|
9
9
|
import { runAnalyseShipmentTask, runDesignPromptTask, runGenerateSpecBatchTask, runGenericTask, runProposeNewCapabilitiesTask, runProposeSkeletonTask, runStructuredModelCall, runVerifyModuleTask } from './api-key-caller.js';
|
|
10
|
-
import { runAnalyseShipmentTaskViaCli, runDesignPromptTaskViaCli, runGenerateSpecBatchTaskViaCli, runGenericTaskViaCli, runProposeNewCapabilitiesTaskViaCli, runProposeSkeletonTaskViaCli, runStructuredModelCallViaCli, runVerifyModuleTaskViaCli } from './claude-cli-caller.js';
|
|
10
|
+
import { runAnalyseShipmentTaskViaCli, runDesignPromptTaskViaCli, runGenerateSpecBatchTaskViaCli, runGenericTaskViaCli, runProposeNewCapabilitiesTaskViaCli, runProposeSkeletonTaskViaCli, runStructuredModelCallViaCli, runVerifyModuleTaskViaCli, runWholeReadModelCallViaCli } from './claude-cli-caller.js';
|
|
11
11
|
import { spawnClaude } from './claude-binary.js';
|
|
12
12
|
import { checkCodexLogin, CODEX_CODE_MODE_MODELS, codexTextModel, defaultCodexHome, runStructuredModelCallViaCodex } from './codex-cli-caller.js';
|
|
13
13
|
import { formatInvestigationReaderDiagnostic, INVESTIGATION_MODEL, runAndReportInvestigation } from './investigation-reader.js';
|
|
14
14
|
import { anthropicTextModel } from './text-model.js';
|
|
15
|
-
import { MappingFailure, runSurfaceMapping, SURFACE_SEED_MODEL } from './surface-mapper.js';
|
|
15
|
+
import { MappingFailure, readSurfaceSnapshotChangeSet, runSurfaceMapping, runSurfaceMappingCheckout, SURFACE_SEED_MODEL, } from './surface-mapper.js';
|
|
16
16
|
import { assertSnapshotReadBoundary, createSandbox } from './sandbox.js';
|
|
17
17
|
import { runGitRangeTask } from './git-range.js';
|
|
18
|
-
import { gitAvailable, githubLabel, interactiveFetch, labelFromCopyPath, ManagedRepoError, managedCopyPath, parseGitHubRepo, prepareManagedCopy, readManagedState, relativeTime, removeManagedCopy, watchManagedCopy, } from './managed-repo.js';
|
|
18
|
+
import { configuredManagedRepository, gitAvailable, githubCloneUrl, githubLabel, interactiveFetch, labelFromCopyPath, ManagedRepoError, managedCopyPath, parseGitHubRepo, prepareManagedCopy, readManagedState, relativeTime, removeManagedCopy, watchManagedCopy, usesTemporaryManagedStorage, } from './managed-repo.js';
|
|
19
19
|
import { isUsageLimitError, UsageLimitPause } from './usage-limit.js';
|
|
20
|
+
import { AttemptSourceCleanupError, createCompanionAttemptSourceManager, startAttemptSourceSweeper, stopAttemptSourceSweeper, withAttemptSource, } from './whole-read-temp.js';
|
|
21
|
+
import { createWholeReadAssignmentPool, } from './whole-read-pool.js';
|
|
22
|
+
import { createWholeReadSharedSourceManager, } from './whole-read-shared-source.js';
|
|
23
|
+
import { checkConnectionRepositoryReadiness, createRepositoryProvider, proveManagedRepositoryAccess, RepositoryProviderError, } from './repository-provider.js';
|
|
24
|
+
import { WholeReadGitError } from './whole-read-git.js';
|
|
25
|
+
import { acquireGithubCompanionSource, acquireAssignedSource, assignmentRouteReadinessCode, acquireLocalSource, dispatchWholeReadClaim, } from './whole-read-runner.js';
|
|
26
|
+
import { WHOLE_READ_PHASE_POLICY } from './whole-read/phase-policy.js';
|
|
20
27
|
import { acquireInstanceLock, installService, isNpxCachePath, releaseInstanceLock, runDaemon, servicePaths, serviceStatus, startService, stopService, uninstallService, } from './service.js';
|
|
21
28
|
const DEFAULT_CODEX_MODEL = 'gpt-6-astra';
|
|
22
29
|
const SURFACE_MAPPING_UNAVAILABLE_STATUSES = new Set([401, 403, 404, 405]);
|
|
@@ -100,6 +107,21 @@ function testCloneUrl(repo) {
|
|
|
100
107
|
const base = process.env.NODE_ENV === 'test' ? process.env.PC_TEST_GITHUB_CLONE_BASE : undefined;
|
|
101
108
|
return base ? `${base.replace(/\/+$/, '')}/${repo.owner}/${repo.name}.git` : undefined;
|
|
102
109
|
}
|
|
110
|
+
async function activateManagedCopyCleanup(url, workspaceId, output = console) {
|
|
111
|
+
try {
|
|
112
|
+
const cleanup = await cleanupMigratedManagedRepositories({
|
|
113
|
+
url, workspaceId, temporaryRepositories: true,
|
|
114
|
+
});
|
|
115
|
+
for (const path of cleanup.removed)
|
|
116
|
+
output.log(`Removed the companion's retired managed copy at ${path}.`);
|
|
117
|
+
for (const pending of cleanup.cleanupPending) {
|
|
118
|
+
output.error(`Managed-copy cleanup_pending at ${pending.path} (${pending.errorCode}); the companion will retry on restart.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
output.error('Managed-copy cleanup_pending; the companion will retry on restart.');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
103
125
|
const restartHint = 'The companion is already running in the background — run "truing stop" then "truing start" so it picks up this new connection.';
|
|
104
126
|
function logRestartHintIfRunning() {
|
|
105
127
|
if (serviceStatus().running)
|
|
@@ -205,43 +227,60 @@ async function cmdConnect(argv) {
|
|
|
205
227
|
console.log(`Found Claude Code: ${version}`);
|
|
206
228
|
}
|
|
207
229
|
const client = new CompanionClient(url, token);
|
|
208
|
-
let
|
|
230
|
+
let serverIdentity;
|
|
209
231
|
try {
|
|
210
|
-
|
|
232
|
+
serverIdentity = await client.whoami();
|
|
211
233
|
}
|
|
212
234
|
catch {
|
|
213
235
|
console.error('Could not verify the companion token. Double check that you copied the whole token, including the part after the dot.');
|
|
214
236
|
process.exitCode = 1;
|
|
215
237
|
return;
|
|
216
238
|
}
|
|
239
|
+
const { workspaceId } = serverIdentity;
|
|
240
|
+
const temporaryManagedRepository = managedRepo !== null && (serverIdentity.temporaryRepositories === true
|
|
241
|
+
|| hasTemporaryManagedRepository({ url, workspaceId, repository: managedRepo }));
|
|
217
242
|
let managedPath = null;
|
|
243
|
+
let managedDefaultBranch = null;
|
|
218
244
|
if (managedRepo) {
|
|
245
|
+
let reconnectMessageShown = false;
|
|
246
|
+
const label = githubLabel(managedRepo);
|
|
247
|
+
const getToken = async () => {
|
|
248
|
+
const response = await client.githubToken(label);
|
|
249
|
+
if (response.reason === 'github_reconnect_needed' && !reconnectMessageShown) {
|
|
250
|
+
reconnectMessageShown = true;
|
|
251
|
+
console.error(response.message ?? 'Reconnect GitHub in the Console.');
|
|
252
|
+
}
|
|
253
|
+
return response.token;
|
|
254
|
+
};
|
|
219
255
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
256
|
+
if (temporaryManagedRepository) {
|
|
257
|
+
const access = await proveManagedRepositoryAccess({
|
|
258
|
+
repository: managedRepo,
|
|
259
|
+
branch: f.branch,
|
|
260
|
+
credential: await getToken(),
|
|
261
|
+
}, { managedRepositoryUrl: (repository) => testCloneUrl(repository) ?? githubCloneUrl(repository) });
|
|
262
|
+
managedDefaultBranch = access.defaultBranch;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const copy = await prepareManagedCopy({
|
|
266
|
+
repo: managedRepo,
|
|
267
|
+
branch: f.branch,
|
|
268
|
+
reposDir: managedReposDir(),
|
|
269
|
+
cloneUrl: testCloneUrl(managedRepo),
|
|
270
|
+
getToken,
|
|
271
|
+
});
|
|
272
|
+
managedPath = copy.path;
|
|
273
|
+
if (copy.fetchOk === false) {
|
|
274
|
+
console.error(reconnectMessageShown
|
|
275
|
+
? `Couldn't bring the copy of ${githubLabel(managedRepo)} up to date just now. The companion keeps trying; if it keeps failing, reconnect GitHub in the Console.`
|
|
276
|
+
: `Couldn't bring the copy of ${githubLabel(managedRepo)} up to date just now. The companion keeps trying; if it keeps failing, run: truing signin`);
|
|
277
|
+
}
|
|
241
278
|
}
|
|
242
279
|
}
|
|
243
280
|
catch (error) {
|
|
244
|
-
console.error(error instanceof ManagedRepoError ? error.message :
|
|
281
|
+
console.error(error instanceof ManagedRepoError ? error.message : temporaryManagedRepository
|
|
282
|
+
? `Couldn't verify access to ${githubLabel(managedRepo)}. Reconnect GitHub in the Console, then run this command again.`
|
|
283
|
+
: `Couldn't copy ${githubLabel(managedRepo)} from GitHub.`);
|
|
245
284
|
process.exitCode = 1;
|
|
246
285
|
return;
|
|
247
286
|
}
|
|
@@ -263,9 +302,46 @@ async function cmdConnect(argv) {
|
|
|
263
302
|
return;
|
|
264
303
|
}
|
|
265
304
|
const requestedFolder = managedPath ?? resolve(f.path || '.');
|
|
266
|
-
const topLevel = f['no-repo'] === 'true'
|
|
305
|
+
const topLevel = f['no-repo'] === 'true' || temporaryManagedRepository
|
|
306
|
+
? null
|
|
307
|
+
: await repositoryTopLevel(requestedFolder);
|
|
267
308
|
let registered = false;
|
|
268
|
-
if (
|
|
309
|
+
if (managedRepo && temporaryManagedRepository && managedDefaultBranch) {
|
|
310
|
+
try {
|
|
311
|
+
const identity = {
|
|
312
|
+
host: 'github.com',
|
|
313
|
+
port: null,
|
|
314
|
+
path: githubLabel(managedRepo),
|
|
315
|
+
defaultBranch: managedDefaultBranch,
|
|
316
|
+
};
|
|
317
|
+
const { connection } = await client.registerConnection(identity);
|
|
318
|
+
addConnection({
|
|
319
|
+
url,
|
|
320
|
+
token,
|
|
321
|
+
workspaceId,
|
|
322
|
+
connection: {
|
|
323
|
+
connectionId: connection.id,
|
|
324
|
+
repoPath: managedCopyPath(managedReposDir(), managedRepo),
|
|
325
|
+
backend,
|
|
326
|
+
codexModel: backend === 'codex' && f['codex-model'] ? codexModel : undefined,
|
|
327
|
+
branch: identity.defaultBranch,
|
|
328
|
+
label: githubLabel(managedRepo),
|
|
329
|
+
managed: true,
|
|
330
|
+
repository: { host: identity.host, port: identity.port, path: identity.path },
|
|
331
|
+
repositoryStorage: 'temporary',
|
|
332
|
+
},
|
|
333
|
+
apiKey: backend === 'codex' ? undefined : apiKey,
|
|
334
|
+
});
|
|
335
|
+
console.log(`Connected ${githubLabel(managedRepo)}. The companion will access it temporarily when work needs it; no permanent code copy was created.`);
|
|
336
|
+
registered = true;
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
console.error(error instanceof PairingHttpError
|
|
340
|
+
? `${error.code}: ${error.message}`
|
|
341
|
+
: 'registration_failed: Could not register this repository with the console.');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else if (topLevel) {
|
|
269
345
|
try {
|
|
270
346
|
const { remote, ...identity } = await repositoryIdentity(topLevel, {
|
|
271
347
|
remote: managedRepo ? 'origin' : f.remote,
|
|
@@ -282,6 +358,7 @@ async function cmdConnect(argv) {
|
|
|
282
358
|
backend,
|
|
283
359
|
codexModel: backend === 'codex' && f['codex-model'] ? codexModel : undefined,
|
|
284
360
|
remote,
|
|
361
|
+
branch: identity.defaultBranch,
|
|
285
362
|
...(managedRepo ? { label: githubLabel(managedRepo), managed: true } : {}),
|
|
286
363
|
},
|
|
287
364
|
apiKey: backend === 'codex' ? undefined : apiKey,
|
|
@@ -307,6 +384,10 @@ async function cmdConnect(argv) {
|
|
|
307
384
|
else if (f['no-repo'] !== 'true') {
|
|
308
385
|
console.log("No project folder here — to connect your code, run this same command inside your project's folder.");
|
|
309
386
|
}
|
|
387
|
+
if (serverIdentity.temporaryRepositories === true) {
|
|
388
|
+
migrateManagedRepositoriesToTemporaryStorage({ url, workspaceId });
|
|
389
|
+
await activateManagedCopyCleanup(url, workspaceId);
|
|
390
|
+
}
|
|
310
391
|
if (!registered) {
|
|
311
392
|
try {
|
|
312
393
|
await client.recordPairingOutcome('workspace_only');
|
|
@@ -366,6 +447,7 @@ async function cmdConnect(argv) {
|
|
|
366
447
|
backend,
|
|
367
448
|
codexModel: backend === 'codex' && f['codex-model'] ? codexModel : undefined,
|
|
368
449
|
remote: f.remote,
|
|
450
|
+
...(f.branch ? { branch: f.branch } : {}),
|
|
369
451
|
},
|
|
370
452
|
apiKey: backend === 'codex' ? undefined : apiKey,
|
|
371
453
|
});
|
|
@@ -604,6 +686,9 @@ export function logInvestigationDiagnostic(diagnostic, output = console) {
|
|
|
604
686
|
output.error(formatInvestigationReaderDiagnostic(diagnostic));
|
|
605
687
|
}
|
|
606
688
|
export function taskRunnerFor(backend, kind) {
|
|
689
|
+
if (kind.startsWith('whole-read-')) {
|
|
690
|
+
return async () => { throw new Error('build_task_kind_unsupported'); };
|
|
691
|
+
}
|
|
607
692
|
if (backend === 'cli') {
|
|
608
693
|
if (kind === 'verify_module')
|
|
609
694
|
return runVerifyModuleTaskViaCli;
|
|
@@ -633,11 +718,18 @@ export function taskRunnerFor(backend, kind) {
|
|
|
633
718
|
return runAnalyseShipmentTask;
|
|
634
719
|
return runGenericTask;
|
|
635
720
|
}
|
|
636
|
-
export async function runTask(task, runtime, repoPath, connection) {
|
|
721
|
+
export async function runTask(task, runtime, repoPath, connection, repositoryLease) {
|
|
637
722
|
if (task.kind === 'git_range') {
|
|
638
723
|
if (!connection)
|
|
639
724
|
return { result: { ok: false, code: 'history_failed' }, filesRead: [] };
|
|
640
|
-
|
|
725
|
+
if (!repositoryLease?.range)
|
|
726
|
+
return runGitRangeTask(task, repoPath, connection);
|
|
727
|
+
return runGitRangeTask(task, repositoryLease.gitDirectory, connection, {
|
|
728
|
+
...repositoryLease.range,
|
|
729
|
+
environment: repositoryLease.gitEnvironment(),
|
|
730
|
+
signal: new AbortController().signal,
|
|
731
|
+
registerReader: (reader) => repositoryLease.registerReader(reader),
|
|
732
|
+
});
|
|
641
733
|
}
|
|
642
734
|
const runner = taskRunnerFor(runtime.backend, task.kind);
|
|
643
735
|
const payload = task.payload;
|
|
@@ -648,6 +740,20 @@ export async function runTask(task, runtime, repoPath, connection) {
|
|
|
648
740
|
}
|
|
649
741
|
if (typeof payload.snapshotSha !== 'string')
|
|
650
742
|
throw new Error('snapshot_unavailable');
|
|
743
|
+
if (repositoryLease) {
|
|
744
|
+
let checkout;
|
|
745
|
+
try {
|
|
746
|
+
checkout = await repositoryLease.checkout();
|
|
747
|
+
await assertSnapshotReadBoundary(checkout.path);
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
throw error instanceof RepositoryProviderError ? error : new Error('snapshot_unavailable');
|
|
751
|
+
}
|
|
752
|
+
const outcome = runtime.backend === 'cli'
|
|
753
|
+
? await runner(task, checkout.path)
|
|
754
|
+
: await runner(runtime.textModel, task, checkout.path);
|
|
755
|
+
return { ...outcome, snapshot: { sha: checkout.commit } };
|
|
756
|
+
}
|
|
651
757
|
let sandbox;
|
|
652
758
|
try {
|
|
653
759
|
sandbox = await createSandbox(repoPath, payload.snapshotSha);
|
|
@@ -679,7 +785,7 @@ function formatCodexUsage(usage) {
|
|
|
679
785
|
return 'unavailable';
|
|
680
786
|
return Object.entries(usage).map(([name, value]) => `${name}=${value}`).join(', ');
|
|
681
787
|
}
|
|
682
|
-
export async function runOneTask(runtime, client, repoPath, task, connection, output = console) {
|
|
788
|
+
export async function runOneTask(runtime, client, repoPath, task, connection, output = console, repositoryProvider) {
|
|
683
789
|
runtime.takeCodexUsage();
|
|
684
790
|
if (runtime.backend === 'codex') {
|
|
685
791
|
output.log(`[${new Date().toISOString()}] claimed ${task.kind} task ${task.id} via codex (dollar budget $${task.maxBudgetUsd.toFixed(2)} is not enforced on this backend)`);
|
|
@@ -687,16 +793,52 @@ export async function runOneTask(runtime, client, repoPath, task, connection, ou
|
|
|
687
793
|
else {
|
|
688
794
|
output.log(`[${new Date().toISOString()}] claimed ${task.kind} task ${task.id} via ${runtime.backend} (budget $${task.maxBudgetUsd.toFixed(2)})`);
|
|
689
795
|
}
|
|
796
|
+
const owner = task.repositoryProvider?.owner;
|
|
797
|
+
const usesTemporaryRepository = repositoryProvider !== undefined && owner !== undefined && connection !== undefined
|
|
798
|
+
&& task.payload.connectionless !== true;
|
|
799
|
+
const controller = new AbortController();
|
|
800
|
+
let lease;
|
|
801
|
+
let leaseLost = false;
|
|
690
802
|
const heartbeat = setInterval(() => {
|
|
691
|
-
void
|
|
803
|
+
void (async () => {
|
|
804
|
+
try {
|
|
805
|
+
const renewed = await client.heartbeat(task.id, usesTemporaryRepository ? owner.claimToken : undefined);
|
|
806
|
+
if (usesTemporaryRepository && lease) {
|
|
807
|
+
if (typeof renewed.expiresAt !== 'string')
|
|
808
|
+
throw new Error('repository_lease_lost');
|
|
809
|
+
await lease.heartbeat({ claimToken: owner.claimToken, expiresAt: renewed.expiresAt });
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
catch (err) {
|
|
813
|
+
leaseLost = true;
|
|
814
|
+
controller.abort();
|
|
815
|
+
output.error(' heartbeat warning:', err instanceof Error ? err.message : err);
|
|
816
|
+
}
|
|
817
|
+
})();
|
|
692
818
|
}, HEARTBEAT_MS);
|
|
693
819
|
try {
|
|
694
|
-
|
|
820
|
+
if (usesTemporaryRepository) {
|
|
821
|
+
await repositoryProvider.sweepRepositories();
|
|
822
|
+
const target = task.kind === 'git_range'
|
|
823
|
+
? { kind: 'range', base: task.payload.base,
|
|
824
|
+
head: String(task.payload.branch ?? '') }
|
|
825
|
+
: { kind: 'snapshot', sha: String(task.payload.snapshotSha ?? '') };
|
|
826
|
+
lease = await repositoryProvider.acquireRepository({
|
|
827
|
+
connection, target, owner, signal: controller.signal,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
const { result, filesRead, snapshot } = await runTask(task, runtime, repoPath, connection, lease);
|
|
831
|
+
if (leaseLost)
|
|
832
|
+
throw new RepositoryProviderError('repository_lease_lost');
|
|
833
|
+
if (lease) {
|
|
834
|
+
await lease.release();
|
|
835
|
+
lease = undefined;
|
|
836
|
+
}
|
|
695
837
|
clearInterval(heartbeat);
|
|
696
838
|
if (task.kind === 'git_range')
|
|
697
839
|
await client.reportGitRangeResult(task.id, result);
|
|
698
840
|
else
|
|
699
|
-
await client.reportResult(task.id, result, filesRead, snapshot);
|
|
841
|
+
await client.reportResult(task.id, result, filesRead, snapshot, usesTemporaryRepository ? owner.claimToken : undefined);
|
|
700
842
|
const done = ` done — reported result (${filesRead.length} file${filesRead.length === 1 ? '' : 's'} read)`;
|
|
701
843
|
output.log(runtime.backend === 'codex'
|
|
702
844
|
? `${done}; token usage: ${formatCodexUsage(runtime.takeCodexUsage())}; dollar budget is not enforced (Codex reports tokens and has no cost cap)`
|
|
@@ -704,7 +846,16 @@ export async function runOneTask(runtime, client, repoPath, task, connection, ou
|
|
|
704
846
|
return null;
|
|
705
847
|
}
|
|
706
848
|
catch (err) {
|
|
707
|
-
|
|
849
|
+
let failure = err;
|
|
850
|
+
if (lease) {
|
|
851
|
+
try {
|
|
852
|
+
await lease.release();
|
|
853
|
+
}
|
|
854
|
+
catch (cleanupError) {
|
|
855
|
+
failure = cleanupError;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
const message = failure instanceof Error ? failure.message : String(failure);
|
|
708
859
|
if (runtime.backend === 'codex' && message.includes('CodexToolUseAttempted')) {
|
|
709
860
|
output.error(` TOOL USE BLOCKED — ${message}`);
|
|
710
861
|
}
|
|
@@ -712,19 +863,40 @@ export async function runOneTask(runtime, client, repoPath, task, connection, ou
|
|
|
712
863
|
output.error(' failed:', message);
|
|
713
864
|
}
|
|
714
865
|
try {
|
|
715
|
-
await client.reportError(task.id, message);
|
|
866
|
+
await client.reportError(task.id, message, usesTemporaryRepository ? owner.claimToken : undefined);
|
|
716
867
|
}
|
|
717
868
|
catch (reportErr) {
|
|
718
869
|
output.error(' could not even report the error back:', reportErr instanceof Error ? reportErr.message : reportErr);
|
|
719
870
|
}
|
|
720
|
-
return runtime.backend === 'cli' && isUsageLimitError(
|
|
871
|
+
return runtime.backend === 'cli' && isUsageLimitError(failure) ? message : null;
|
|
721
872
|
}
|
|
722
873
|
finally {
|
|
723
874
|
clearInterval(heartbeat);
|
|
724
875
|
}
|
|
725
876
|
}
|
|
877
|
+
// Every pairing shares one local lifecycle registry. Separate managers pointed at the same root
|
|
878
|
+
// would mistake another Console's live source for an orphan during a sweep.
|
|
879
|
+
const repositoryAttemptSourceManager = createCompanionAttemptSourceManager({
|
|
880
|
+
root: join(dirname(configPath), 'repository-attempts'),
|
|
881
|
+
// The Console's signed claim metadata gates acquisition; this is never invoked while the
|
|
882
|
+
// server-side rollout is off.
|
|
883
|
+
buildsEnabled: () => true,
|
|
884
|
+
});
|
|
885
|
+
function ordinaryTaskRepositoryProvider(pairing, client) {
|
|
886
|
+
return createRepositoryProvider({
|
|
887
|
+
sourceManager: repositoryAttemptSourceManager,
|
|
888
|
+
workspaceId: pairing.workspaceId ?? 'unpaired',
|
|
889
|
+
hostId: pairing.workspaceId ?? 'companion',
|
|
890
|
+
// Renewal first heartbeats the server below. A rejected heartbeat aborts this attempt.
|
|
891
|
+
isClaimLive: async () => true,
|
|
892
|
+
getCredential: async (_connection, repository) => (await client.githubToken(githubLabel(repository))).token,
|
|
893
|
+
managedRepositoryUrl: (repository) => testCloneUrl(repository) ?? githubCloneUrl(repository),
|
|
894
|
+
});
|
|
895
|
+
}
|
|
726
896
|
export function backendForInvestigation(request, connections) {
|
|
727
897
|
for (const source of request.requestedSources) {
|
|
898
|
+
if (source.kind === 'prd')
|
|
899
|
+
continue;
|
|
728
900
|
const configured = connections.find((connection) => connection.connectionId === source.connectionId);
|
|
729
901
|
if (configured)
|
|
730
902
|
return configured.backend;
|
|
@@ -740,13 +912,14 @@ export function structuredModelCallerForRuntime(runtime, anthropic, callViaCodex
|
|
|
740
912
|
}
|
|
741
913
|
return (request) => runStructuredModelCall(anthropic, request);
|
|
742
914
|
}
|
|
743
|
-
async function runOneInvestigation(anthropic, client, connections, runtimes, request, output = console) {
|
|
915
|
+
async function runOneInvestigation(anthropic, client, connections, runtimes, request, output = console, repositoryProvider) {
|
|
744
916
|
const backend = backendForInvestigation(request, connections);
|
|
745
917
|
output.log(`[${new Date().toISOString()}] claimed investigation ${request.investigationId} attempt ${request.attempt} via ${backend} (${request.obligations.length} obligation${request.obligations.length === 1 ? '' : 's'})`);
|
|
746
918
|
if (Date.now() >= Date.parse(request.claimExpiresAt)) {
|
|
747
919
|
output.error(` CLAIM LEASE WARNING: ${request.investigationId} was already expired at ${request.claimExpiresAt}; the result may be rejected.`);
|
|
748
920
|
}
|
|
749
921
|
const runtime = request.requestedSources
|
|
922
|
+
.filter((source) => source.kind !== 'prd')
|
|
750
923
|
.map((source) => runtimes.get(source.connectionId))
|
|
751
924
|
.find((candidate) => !!candidate);
|
|
752
925
|
const baseCallModel = runtime
|
|
@@ -769,6 +942,10 @@ async function runOneInvestigation(anthropic, client, connections, runtimes, req
|
|
|
769
942
|
}
|
|
770
943
|
: baseCallModel;
|
|
771
944
|
const validationRejectedObligationIds = new Set();
|
|
945
|
+
const controller = new AbortController();
|
|
946
|
+
const repositoryOwner = request.repositoryProvider
|
|
947
|
+
? { ...request.repositoryProvider.owner, expiresAt: request.claimExpiresAt }
|
|
948
|
+
: undefined;
|
|
772
949
|
try {
|
|
773
950
|
const result = await runAndReportInvestigation(request, connections, callModel, async (envelope) => {
|
|
774
951
|
if (Date.now() >= Date.parse(request.claimExpiresAt)) {
|
|
@@ -790,26 +967,116 @@ async function runOneInvestigation(anthropic, client, connections, runtimes, req
|
|
|
790
967
|
model: runtime?.backend === 'codex'
|
|
791
968
|
? runtime.codexOptions?.model
|
|
792
969
|
: INVESTIGATION_MODEL,
|
|
793
|
-
}
|
|
970
|
+
}, repositoryProvider && repositoryOwner
|
|
971
|
+
? { repositoryProvider, owner: repositoryOwner, signal: controller.signal }
|
|
972
|
+
: undefined);
|
|
794
973
|
logInvestigationResult(result, output, validationRejectedObligationIds);
|
|
795
974
|
}
|
|
796
975
|
catch (error) {
|
|
797
976
|
output.error(` RESULT NOT DELIVERED for ${request.investigationId} attempt ${request.attempt}; no automatic retry will be made and the claim must expire:`, error instanceof Error ? error.message : error);
|
|
798
977
|
}
|
|
978
|
+
finally {
|
|
979
|
+
controller.abort();
|
|
980
|
+
}
|
|
799
981
|
return usageLimitMessage;
|
|
800
982
|
}
|
|
801
|
-
async function runOneSurfaceMapping(anthropic, client, connection, runtime, job, output = console) {
|
|
983
|
+
export async function runOneSurfaceMapping(anthropic, client, connection, runtime, job, output = console, repositoryProvider, options = {}) {
|
|
802
984
|
output.log(`[${new Date().toISOString()}] claimed surface mapping ${job.jobId} attempt ${job.attempt} via ${runtime.backend}`);
|
|
985
|
+
const owner = job.repositoryProvider?.owner;
|
|
986
|
+
const usesTemporaryRepository = repositoryProvider !== undefined && owner !== undefined;
|
|
987
|
+
const controller = new AbortController();
|
|
988
|
+
const leases = [];
|
|
989
|
+
let leaseLost = false;
|
|
990
|
+
const releaseLeases = async () => {
|
|
991
|
+
const pending = leases.splice(0).reverse();
|
|
992
|
+
let failure;
|
|
993
|
+
for (const lease of pending) {
|
|
994
|
+
try {
|
|
995
|
+
await lease.release();
|
|
996
|
+
}
|
|
997
|
+
catch (error) {
|
|
998
|
+
failure ??= error;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (failure)
|
|
1002
|
+
throw failure;
|
|
1003
|
+
};
|
|
1004
|
+
const heartbeat = usesTemporaryRepository ? setInterval(() => {
|
|
1005
|
+
void (async () => {
|
|
1006
|
+
try {
|
|
1007
|
+
const renewed = await client.heartbeatSurfaceMapping(job);
|
|
1008
|
+
for (const lease of leases) {
|
|
1009
|
+
await lease.heartbeat({ claimToken: owner.claimToken, expiresAt: renewed.expiresAt });
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
catch (error) {
|
|
1013
|
+
leaseLost = true;
|
|
1014
|
+
controller.abort();
|
|
1015
|
+
output.error(' heartbeat warning:', error instanceof Error ? error.message : error);
|
|
1016
|
+
}
|
|
1017
|
+
})();
|
|
1018
|
+
}, options.heartbeatMs ?? HEARTBEAT_MS) : null;
|
|
803
1019
|
try {
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1020
|
+
const callModel = options.callModel ?? structuredModelCallerForRuntime(runtime, anthropic);
|
|
1021
|
+
const execution = {
|
|
1022
|
+
backend: runtime.backend,
|
|
1023
|
+
model: runtime.backend === 'codex' ? runtime.codexOptions?.model ?? DEFAULT_CODEX_MODEL : SURFACE_SEED_MODEL,
|
|
1024
|
+
};
|
|
1025
|
+
let manifest;
|
|
1026
|
+
if (usesTemporaryRepository) {
|
|
1027
|
+
await repositoryProvider.sweepRepositories();
|
|
1028
|
+
const headLease = await repositoryProvider.acquireRepository({
|
|
1029
|
+
connection, target: { kind: 'snapshot', sha: job.commit }, owner, signal: controller.signal,
|
|
1030
|
+
});
|
|
1031
|
+
leases.push(headLease);
|
|
1032
|
+
const head = await headLease.checkout();
|
|
1033
|
+
let reuseChangeSet = null;
|
|
1034
|
+
if (job.reuseCandidate) {
|
|
1035
|
+
try {
|
|
1036
|
+
const baseLease = await repositoryProvider.acquireRepository({
|
|
1037
|
+
connection,
|
|
1038
|
+
target: { kind: 'snapshot', sha: job.reuseCandidate.commit },
|
|
1039
|
+
owner,
|
|
1040
|
+
signal: controller.signal,
|
|
1041
|
+
});
|
|
1042
|
+
leases.push(baseLease);
|
|
1043
|
+
const base = await baseLease.checkout();
|
|
1044
|
+
reuseChangeSet = await readSurfaceSnapshotChangeSet(base.path, base.commit, head.path, head.commit);
|
|
1045
|
+
}
|
|
1046
|
+
catch (error) {
|
|
1047
|
+
if (!(error instanceof RepositoryProviderError)
|
|
1048
|
+
|| !['snapshot_unavailable', 'repository_access_denied'].includes(error.code))
|
|
1049
|
+
throw error;
|
|
1050
|
+
reuseChangeSet = null;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
manifest = await runSurfaceMappingCheckout({
|
|
1054
|
+
claim: job,
|
|
1055
|
+
checkoutPath: head.path,
|
|
1056
|
+
callModel,
|
|
1057
|
+
execution,
|
|
1058
|
+
reuseChangeSet,
|
|
1059
|
+
assertLive: () => {
|
|
1060
|
+
if (leaseLost || controller.signal.aborted)
|
|
1061
|
+
throw new RepositoryProviderError('repository_lease_lost');
|
|
1062
|
+
for (const lease of leases)
|
|
1063
|
+
lease.assertLive();
|
|
1064
|
+
},
|
|
1065
|
+
});
|
|
1066
|
+
if (leaseLost)
|
|
1067
|
+
throw new RepositoryProviderError('repository_lease_lost');
|
|
1068
|
+
if (heartbeat)
|
|
1069
|
+
clearInterval(heartbeat);
|
|
1070
|
+
await releaseLeases();
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
manifest = await runSurfaceMapping({
|
|
1074
|
+
claim: job,
|
|
1075
|
+
repoPath: connection.repoPath,
|
|
1076
|
+
callModel,
|
|
1077
|
+
execution,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
813
1080
|
let acknowledgment;
|
|
814
1081
|
try {
|
|
815
1082
|
acknowledgment = await client.reportSurfaceMappingResult(job, manifest);
|
|
@@ -826,16 +1093,29 @@ async function runOneSurfaceMapping(anthropic, client, connection, runtime, job,
|
|
|
826
1093
|
return null;
|
|
827
1094
|
}
|
|
828
1095
|
catch (error) {
|
|
1096
|
+
let failure = error;
|
|
1097
|
+
try {
|
|
1098
|
+
await releaseLeases();
|
|
1099
|
+
}
|
|
1100
|
+
catch (cleanupError) {
|
|
1101
|
+
failure = cleanupError;
|
|
1102
|
+
}
|
|
829
1103
|
const usageLimitMessage = runtime.backend === 'cli' && isUsageLimitError(error)
|
|
830
1104
|
? (error instanceof Error ? error.message : String(error))
|
|
831
1105
|
: null;
|
|
832
|
-
const report =
|
|
833
|
-
? { kind:
|
|
834
|
-
:
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1106
|
+
const report = failure instanceof MappingFailure
|
|
1107
|
+
? { kind: failure.kind, code: failure.code, detail: failure.message }
|
|
1108
|
+
: failure instanceof RepositoryProviderError
|
|
1109
|
+
? {
|
|
1110
|
+
kind: failure.code === 'snapshot_unavailable' ? 'stable' : 'transient',
|
|
1111
|
+
code: failure.code,
|
|
1112
|
+
detail: failure.code,
|
|
1113
|
+
}
|
|
1114
|
+
: {
|
|
1115
|
+
kind: 'transient',
|
|
1116
|
+
code: 'mapper_crashed',
|
|
1117
|
+
detail: failure instanceof Error ? failure.message : String(failure),
|
|
1118
|
+
};
|
|
839
1119
|
output.error(` surface mapping ${job.jobId} failed (${report.code}): ${report.detail}`);
|
|
840
1120
|
try {
|
|
841
1121
|
await client.reportSurfaceMappingError(job, report);
|
|
@@ -850,6 +1130,219 @@ async function runOneSurfaceMapping(anthropic, client, connection, runtime, job,
|
|
|
850
1130
|
}
|
|
851
1131
|
return usageLimitMessage;
|
|
852
1132
|
}
|
|
1133
|
+
finally {
|
|
1134
|
+
if (heartbeat)
|
|
1135
|
+
clearInterval(heartbeat);
|
|
1136
|
+
controller.abort();
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
export function wholeReadModelRequest(request, callContext) {
|
|
1140
|
+
const policy = WHOLE_READ_PHASE_POLICY[request.phase];
|
|
1141
|
+
const thinking = policy.thinking[request.model] ?? 'adaptive';
|
|
1142
|
+
return { purpose: 'whole-read', model: request.model, effort: policy.effort, thinking,
|
|
1143
|
+
maxTokens: request.maxTokens, system: '', stableText: '', text: request.prompt, schema: request.schema,
|
|
1144
|
+
wholeRead: callContext };
|
|
1145
|
+
}
|
|
1146
|
+
function wholeReadBackend(runtime, callContext) {
|
|
1147
|
+
return async (request) => {
|
|
1148
|
+
const modelRequest = wholeReadModelRequest(request, callContext);
|
|
1149
|
+
const reply = runtime.backend === 'cli'
|
|
1150
|
+
? await runWholeReadModelCallViaCli(modelRequest)
|
|
1151
|
+
: await runtime.textModel(modelRequest);
|
|
1152
|
+
let result;
|
|
1153
|
+
try {
|
|
1154
|
+
result = JSON.parse(reply.text);
|
|
1155
|
+
}
|
|
1156
|
+
catch {
|
|
1157
|
+
throw new Error('note_schema_invalid');
|
|
1158
|
+
}
|
|
1159
|
+
const inputTokens = reply.usage?.input_tokens ?? reply.usage?.inputTokens;
|
|
1160
|
+
const outputTokens = reply.usage?.output_tokens ?? reply.usage?.outputTokens;
|
|
1161
|
+
const usage = Number.isSafeInteger(inputTokens) && Number(inputTokens) >= 0
|
|
1162
|
+
&& Number.isSafeInteger(outputTokens) && Number(outputTokens) >= 0
|
|
1163
|
+
? { inputTokens: Number(inputTokens), outputTokens: Number(outputTokens) } : null;
|
|
1164
|
+
return { result, usage, metadata: reply.metadata };
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function wholeReadTransport(client, onUsageLimit) {
|
|
1168
|
+
return {
|
|
1169
|
+
putPage: (claim, page) => client.putBuildResultPage(claim, page),
|
|
1170
|
+
seal: (claim, request) => client.sealBuildResult(claim, request),
|
|
1171
|
+
accept: (claim, request) => client.acceptBuildResult(claim, request),
|
|
1172
|
+
fail: async (claim, failure, usage) => {
|
|
1173
|
+
if (failure.code === 'usage_limit')
|
|
1174
|
+
onUsageLimit();
|
|
1175
|
+
const result = await client.reportBuildError(claim, failure, usage);
|
|
1176
|
+
if (result.status === 'retrying')
|
|
1177
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
1178
|
+
return result;
|
|
1179
|
+
},
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
function wholeReadBackendName(runtime) {
|
|
1183
|
+
return sharedWholeReadBackendName(runtime.backend);
|
|
1184
|
+
}
|
|
1185
|
+
async function runWholeReadAssignment(input) {
|
|
1186
|
+
const connection = input.connections.find((candidate) => candidate.connectionId === input.assignment.connectionId);
|
|
1187
|
+
const local = connection && !connection.managed ? connection : undefined;
|
|
1188
|
+
const runtime = [
|
|
1189
|
+
connection ? input.runtimes.get(connection.connectionId) : undefined,
|
|
1190
|
+
input.workspaceRuntime ?? undefined,
|
|
1191
|
+
...input.runtimes.values(),
|
|
1192
|
+
].find((candidate) => candidate !== undefined
|
|
1193
|
+
&& wholeReadBackendName(candidate) === input.assignment.frozenBackend);
|
|
1194
|
+
const backendReady = runtime !== undefined
|
|
1195
|
+
&& wholeReadBackendName(runtime) === input.assignment.frozenBackend;
|
|
1196
|
+
const routeReadiness = assignmentRouteReadinessCode(input.assignment.route);
|
|
1197
|
+
if (routeReadiness !== 'ready') {
|
|
1198
|
+
await input.client.acceptBuildAssignment(input.assignment, {
|
|
1199
|
+
lsRemoteVerified: false, exactCommitFetched: false,
|
|
1200
|
+
backend: input.assignment.frozenBackend, backendReady,
|
|
1201
|
+
routeSupported: false,
|
|
1202
|
+
});
|
|
1203
|
+
return { status: 'not_claimed' };
|
|
1204
|
+
}
|
|
1205
|
+
if (input.assignment.state === 'offered') {
|
|
1206
|
+
let verified = false;
|
|
1207
|
+
try {
|
|
1208
|
+
const acquired = await input.sharedSources.acquire(wholeReadSharedSourceKey(input.pairing, input.assignment), async (lease, signal) => {
|
|
1209
|
+
const source = await acquireAssignedSource({
|
|
1210
|
+
route: input.assignment.route,
|
|
1211
|
+
acquireLocal: () => local?.branch
|
|
1212
|
+
? acquireLocalSource({ repoPath: local.repoPath, branch: local.branch,
|
|
1213
|
+
snapshotSha: input.assignment.snapshotSha, signal })
|
|
1214
|
+
: Promise.reject(new WholeReadGitError('snapshot_unavailable')),
|
|
1215
|
+
acquireGithubCompanion: () => {
|
|
1216
|
+
const repositoryUrl = `https://github.com/${githubLabel(parseGitHubRepo(input.assignment.canonicalRepository))}.git`;
|
|
1217
|
+
return acquireGithubCompanionSource({ lease, repositoryUrl,
|
|
1218
|
+
snapshotSha: input.assignment.snapshotSha, signal });
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
if (!source)
|
|
1222
|
+
throw new WholeReadGitError('snapshot_unavailable');
|
|
1223
|
+
return source;
|
|
1224
|
+
});
|
|
1225
|
+
await acquired.release();
|
|
1226
|
+
verified = true;
|
|
1227
|
+
}
|
|
1228
|
+
catch {
|
|
1229
|
+
verified = false;
|
|
1230
|
+
}
|
|
1231
|
+
const acceptance = await input.client.acceptBuildAssignment(input.assignment, {
|
|
1232
|
+
lsRemoteVerified: verified, exactCommitFetched: verified,
|
|
1233
|
+
backend: input.assignment.frozenBackend, backendReady,
|
|
1234
|
+
routeSupported: routeReadiness === 'ready',
|
|
1235
|
+
});
|
|
1236
|
+
if (acceptance.status !== 'ready')
|
|
1237
|
+
return { status: 'not_claimed' };
|
|
1238
|
+
}
|
|
1239
|
+
if (!runtime || !backendReady || input.shouldStop())
|
|
1240
|
+
return { status: 'not_claimed' };
|
|
1241
|
+
const claimed = await input.client.claimBuildUnit(input.assignment, {
|
|
1242
|
+
backend: input.assignment.frozenBackend, backendReady: true,
|
|
1243
|
+
payerIdentities: input.assignment.payerIdentity ? [input.assignment.payerIdentity] : [],
|
|
1244
|
+
});
|
|
1245
|
+
if (claimed.status !== 'claimed')
|
|
1246
|
+
return {
|
|
1247
|
+
status: claimed.status === 'build_concurrency_limit' ? 'build_concurrency_limit' : 'not_claimed',
|
|
1248
|
+
};
|
|
1249
|
+
const claim = claimed;
|
|
1250
|
+
const controller = new AbortController();
|
|
1251
|
+
if (claim.kind === 'group' || claim.kind === 'prd') {
|
|
1252
|
+
try {
|
|
1253
|
+
await withAttemptSource({
|
|
1254
|
+
workspaceId: input.assignment.workspaceId,
|
|
1255
|
+
buildId: input.assignment.buildId,
|
|
1256
|
+
attemptId: claim.attemptId,
|
|
1257
|
+
hostId: input.pairing.workspaceId ?? 'companion',
|
|
1258
|
+
leaseExpiresAt: claim.leaseExpiresAt,
|
|
1259
|
+
}, async (lease) => dispatchWholeReadClaim(claim, {
|
|
1260
|
+
backend: wholeReadBackend(runtime, {
|
|
1261
|
+
attemptDirectory: lease.path,
|
|
1262
|
+
signal: controller.signal,
|
|
1263
|
+
registerReader: (reader) => lease.registerReader(reader),
|
|
1264
|
+
}),
|
|
1265
|
+
transport: wholeReadTransport(input.client, input.onUsageLimit),
|
|
1266
|
+
signal: controller.signal,
|
|
1267
|
+
}));
|
|
1268
|
+
return { status: 'claimed' };
|
|
1269
|
+
}
|
|
1270
|
+
catch (reason) {
|
|
1271
|
+
if (reason instanceof AttemptSourceCleanupError) {
|
|
1272
|
+
await input.client.reportBuildError(claim, { code: 'cleanup_pending', source: 'companion' });
|
|
1273
|
+
return { status: 'claimed' };
|
|
1274
|
+
}
|
|
1275
|
+
await input.client.reportBuildError(claim, {
|
|
1276
|
+
code: 'capability_failure', source: wholeReadBackendName(runtime),
|
|
1277
|
+
});
|
|
1278
|
+
return { status: 'claimed' };
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
try {
|
|
1282
|
+
await withAttemptSource({
|
|
1283
|
+
workspaceId: input.assignment.workspaceId,
|
|
1284
|
+
buildId: input.assignment.buildId,
|
|
1285
|
+
attemptId: claim.attemptId,
|
|
1286
|
+
hostId: input.pairing.workspaceId ?? 'companion',
|
|
1287
|
+
leaseExpiresAt: claim.leaseExpiresAt,
|
|
1288
|
+
}, async (lease) => {
|
|
1289
|
+
const shared = await input.sharedSources.acquire(wholeReadSharedSourceKey(input.pairing, input.assignment), async (sourceLease, signal) => {
|
|
1290
|
+
const source = await acquireAssignedSource({
|
|
1291
|
+
route: input.assignment.route,
|
|
1292
|
+
acquireLocal: () => local?.branch
|
|
1293
|
+
? acquireLocalSource({ repoPath: local.repoPath, branch: local.branch,
|
|
1294
|
+
snapshotSha: input.assignment.snapshotSha, signal })
|
|
1295
|
+
: Promise.reject(new WholeReadGitError('snapshot_unavailable')),
|
|
1296
|
+
acquireGithubCompanion: async () => {
|
|
1297
|
+
const repositoryUrl = `https://github.com/${githubLabel(parseGitHubRepo(input.assignment.canonicalRepository))}.git`;
|
|
1298
|
+
return acquireGithubCompanionSource({ lease: sourceLease, repositoryUrl,
|
|
1299
|
+
snapshotSha: input.assignment.snapshotSha, signal });
|
|
1300
|
+
},
|
|
1301
|
+
});
|
|
1302
|
+
if (!source)
|
|
1303
|
+
throw new WholeReadGitError('snapshot_unavailable');
|
|
1304
|
+
return source;
|
|
1305
|
+
}, controller.signal);
|
|
1306
|
+
try {
|
|
1307
|
+
await dispatchWholeReadClaim(claim, { source: shared.source,
|
|
1308
|
+
backend: wholeReadBackend(runtime, {
|
|
1309
|
+
attemptDirectory: lease.path,
|
|
1310
|
+
signal: controller.signal,
|
|
1311
|
+
registerReader: (reader) => lease.registerReader(reader),
|
|
1312
|
+
}),
|
|
1313
|
+
transport: wholeReadTransport(input.client, input.onUsageLimit), signal: controller.signal });
|
|
1314
|
+
}
|
|
1315
|
+
finally {
|
|
1316
|
+
await shared.release();
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
return { status: 'claimed' };
|
|
1320
|
+
}
|
|
1321
|
+
catch (reason) {
|
|
1322
|
+
if (reason instanceof AttemptSourceCleanupError) {
|
|
1323
|
+
await input.client.reportBuildError(claim, { code: 'cleanup_pending', source: 'companion' });
|
|
1324
|
+
}
|
|
1325
|
+
else if (reason instanceof WholeReadGitError || (reason instanceof Error
|
|
1326
|
+
&& reason.message === 'snapshot_unavailable')) {
|
|
1327
|
+
await input.client.reportBuildError(claim, { code: 'snapshot_unavailable', source: 'companion' });
|
|
1328
|
+
}
|
|
1329
|
+
else {
|
|
1330
|
+
await input.client.reportBuildError(claim, {
|
|
1331
|
+
code: 'capability_failure', source: wholeReadBackendName(runtime),
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
return { status: 'claimed' };
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
function wholeReadSharedSourceKey(pairing, assignment) {
|
|
1338
|
+
return {
|
|
1339
|
+
workspaceId: assignment.workspaceId,
|
|
1340
|
+
buildId: assignment.buildId,
|
|
1341
|
+
assignmentId: assignment.id,
|
|
1342
|
+
assignmentRevision: assignment.assignmentRevision,
|
|
1343
|
+
snapshotSha: assignment.snapshotSha,
|
|
1344
|
+
hostId: pairing.workspaceId ?? 'companion',
|
|
1345
|
+
};
|
|
853
1346
|
}
|
|
854
1347
|
function pairingPrefix(pairing) {
|
|
855
1348
|
try {
|
|
@@ -864,16 +1357,25 @@ async function runPairing(pairing, options) {
|
|
|
864
1357
|
const log = (...values) => console.log(prefix + String(values[0] ?? ''), ...values.slice(1));
|
|
865
1358
|
const error = (...values) => console.error(prefix + String(values[0] ?? ''), ...values.slice(1));
|
|
866
1359
|
const warn = (...values) => console.warn(prefix + String(values[0] ?? ''), ...values.slice(1));
|
|
867
|
-
const
|
|
1360
|
+
const fastModelsByConnection = Object.fromEntries(pairing.connections.flatMap((connection) => {
|
|
1361
|
+
if (connection.backend === 'cli')
|
|
1362
|
+
return [[connection.connectionId, CLAUDE_CLI_FAST_MODEL]];
|
|
1363
|
+
if (connection.backend === 'codex')
|
|
1364
|
+
return [[connection.connectionId, CODEX_FAST_MODEL]];
|
|
1365
|
+
return [];
|
|
1366
|
+
}));
|
|
1367
|
+
const backendsByConnection = Object.fromEntries(pairing.connections.map((connection) => [connection.connectionId, sharedWholeReadBackendName(connection.backend)]));
|
|
1368
|
+
const client = new CompanionClient(pairing.url, pairing.token, fastModelsByConnection, backendsByConnection);
|
|
868
1369
|
const tokenRejected = () => {
|
|
869
1370
|
error(`The Console at ${pairing.url} no longer accepts this computer's token — stopped working for it. Pair again from that Console to reconnect.`);
|
|
870
1371
|
return 'token_rejected';
|
|
871
1372
|
};
|
|
1373
|
+
let serverIdentity;
|
|
872
1374
|
if (options.multiple) {
|
|
873
1375
|
let failures = 0;
|
|
874
1376
|
while (!options.shouldStop()) {
|
|
875
1377
|
try {
|
|
876
|
-
await client.whoami();
|
|
1378
|
+
serverIdentity = await client.whoami();
|
|
877
1379
|
break;
|
|
878
1380
|
}
|
|
879
1381
|
catch (reason) {
|
|
@@ -889,13 +1391,23 @@ async function runPairing(pairing, options) {
|
|
|
889
1391
|
return 'stopped';
|
|
890
1392
|
}
|
|
891
1393
|
else {
|
|
892
|
-
await client.whoami();
|
|
1394
|
+
serverIdentity = await client.whoami();
|
|
1395
|
+
}
|
|
1396
|
+
const temporaryRepositories = serverIdentity.temporaryRepositories === true;
|
|
1397
|
+
if (temporaryRepositories) {
|
|
1398
|
+
const migrated = migrateManagedRepositoriesToTemporaryStorage({
|
|
1399
|
+
url: pairing.url,
|
|
1400
|
+
workspaceId: serverIdentity.workspaceId,
|
|
1401
|
+
});
|
|
1402
|
+
if (migrated)
|
|
1403
|
+
pairing.connections = migrated.connections;
|
|
1404
|
+
await activateManagedCopyCleanup(pairing.url, serverIdentity.workspaceId, { log, error });
|
|
893
1405
|
}
|
|
894
1406
|
const workspaceBackend = pairing.workspaceBackend ?? 'api-key';
|
|
895
1407
|
const activeConnections = pairing.connections.filter((connection) => !options.unavailableConnections.has(connection.connectionId));
|
|
896
1408
|
for (const connection of activeConnections) {
|
|
897
|
-
if (connection.managed) {
|
|
898
|
-
const label =
|
|
1409
|
+
if (connection.managed && !temporaryRepositories && !usesTemporaryManagedStorage(connection)) {
|
|
1410
|
+
const label = githubLabel(configuredManagedRepository(connection));
|
|
899
1411
|
watchManagedCopy(connection.repoPath, label, (message) => error(`[${new Date().toISOString()}] ${message}`), () => client.githubToken(label));
|
|
900
1412
|
}
|
|
901
1413
|
}
|
|
@@ -903,13 +1415,18 @@ async function runPairing(pairing, options) {
|
|
|
903
1415
|
connection.connectionId,
|
|
904
1416
|
createBackendRuntime(connection, options.anthropic),
|
|
905
1417
|
]));
|
|
1418
|
+
const repositoryProvider = ordinaryTaskRepositoryProvider(pairing, client);
|
|
1419
|
+
const repositoryReadinessFor = (connection) => checkConnectionRepositoryReadiness(connection, temporaryRepositories, {
|
|
1420
|
+
getCredential: async (_configured, repository) => (await client.githubToken(githubLabel(repository))).token,
|
|
1421
|
+
managedRepositoryUrl: (repository) => testCloneUrl(repository) ?? githubCloneUrl(repository),
|
|
1422
|
+
});
|
|
906
1423
|
const workspaceRuntime = pairing.pollWorkspaceTasks && !options.workspaceTasksUnavailable
|
|
907
1424
|
? createBackendRuntime({ backend: workspaceBackend, codexModel: pairing.codexModel }, options.anthropic)
|
|
908
1425
|
: null;
|
|
909
1426
|
const repositoryReadiness = new Map();
|
|
910
1427
|
for (const connection of activeConnections) {
|
|
911
1428
|
repositoryReadiness.set(connection.connectionId, {
|
|
912
|
-
readiness: await
|
|
1429
|
+
readiness: await repositoryReadinessFor(connection),
|
|
913
1430
|
checkedAt: Date.now(),
|
|
914
1431
|
});
|
|
915
1432
|
}
|
|
@@ -924,6 +1441,10 @@ async function runPairing(pairing, options) {
|
|
|
924
1441
|
for (const connection of activeConnections) {
|
|
925
1442
|
const readiness = repositoryReadiness.get(connection.connectionId).readiness;
|
|
926
1443
|
if (readiness.state === 'ready') {
|
|
1444
|
+
if (temporaryRepositories && connection.managed) {
|
|
1445
|
+
log(` - ${connection.connectionId} (${githubLabel(configuredManagedRepository(connection))}; temporary repository access ready) (${connection.backend})`);
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
927
1448
|
const sha = await currentCommit(connection.repoPath).catch(() => '(unknown)');
|
|
928
1449
|
log(options.safeStartupDiagnostics
|
|
929
1450
|
? ` - ${connection.connectionId} @ ${sha.slice(0, 7)} (${connection.backend})`
|
|
@@ -950,6 +1471,7 @@ async function runPairing(pairing, options) {
|
|
|
950
1471
|
let investigationPollFailures = 0;
|
|
951
1472
|
let nextInvestigationPollAt = 0;
|
|
952
1473
|
let surfaceMappingPollingEnabled = true;
|
|
1474
|
+
let wholeReadBuildPollingEnabled = true;
|
|
953
1475
|
const skippedConnections = new Set();
|
|
954
1476
|
const loggedSkippedConnections = new Set();
|
|
955
1477
|
const readinessReporting = {
|
|
@@ -964,164 +1486,217 @@ async function runPairing(pairing, options) {
|
|
|
964
1486
|
loggedSkippedConnections.add(connectionId);
|
|
965
1487
|
error(`Project ${connectionId} isn't part of the workspace at ${pairing.url} anymore — skipping it. Run connect again from that project's folder to re-add it.`);
|
|
966
1488
|
};
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1489
|
+
let usageClaimLimit = 0;
|
|
1490
|
+
let asyncTokenRejected = false;
|
|
1491
|
+
const mayClaim = () => usageClaimLimit === null || usageClaimLimit > 0;
|
|
1492
|
+
const recordCompletedClaim = (usageLimitMessage) => {
|
|
1493
|
+
if (usageClaimLimit !== null)
|
|
1494
|
+
usageClaimLimit--;
|
|
1495
|
+
if (usageLimitMessage) {
|
|
1496
|
+
options.usageLimitPause.pause(usageLimitMessage);
|
|
1497
|
+
usageClaimLimit = 0;
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
const sharedSources = createWholeReadSharedSourceManager({
|
|
1501
|
+
onCleanupPending: () => { error('[whole-read-temp] cleanup_pending'); },
|
|
1502
|
+
});
|
|
1503
|
+
const wholeReadPool = createWholeReadAssignmentPool({
|
|
1504
|
+
key: (assignment) => assignment.id,
|
|
1505
|
+
mayClaim,
|
|
1506
|
+
shouldStop: options.shouldStop,
|
|
1507
|
+
onStart: () => { if (usageClaimLimit !== null)
|
|
1508
|
+
usageClaimLimit--; },
|
|
1509
|
+
run: (assignment) => runWholeReadAssignment({ pairing, client, assignment,
|
|
1510
|
+
connections: activeConnections, runtimes, workspaceRuntime, sharedSources,
|
|
1511
|
+
onUsageLimit: () => recordCompletedClaim('usage limit reached'),
|
|
1512
|
+
shouldStop: options.shouldStop }),
|
|
1513
|
+
onError: (reason) => {
|
|
1514
|
+
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1515
|
+
asyncTokenRejected = true;
|
|
1516
|
+
else
|
|
1517
|
+
error('Whole-read build attempt failed:', reason instanceof Error ? reason.message : reason);
|
|
1518
|
+
},
|
|
1519
|
+
});
|
|
1520
|
+
try {
|
|
1521
|
+
while (!options.shouldStop()) {
|
|
1522
|
+
try {
|
|
1523
|
+
if (asyncTokenRejected)
|
|
1524
|
+
return tokenRejected();
|
|
1525
|
+
let claimedAny = false;
|
|
1526
|
+
const pollableConnections = activeConnections.filter((connection) => !skippedConnections.has(connection.connectionId));
|
|
1527
|
+
const readyConnections = [];
|
|
1528
|
+
const readinessNow = Date.now();
|
|
1529
|
+
for (const connection of pollableConnections) {
|
|
1530
|
+
if (options.shouldStop())
|
|
1531
|
+
break;
|
|
1532
|
+
let cached = repositoryReadiness.get(connection.connectionId);
|
|
1533
|
+
if (readinessNow - cached.checkedAt >= REPOSITORY_RECHECK_MS) {
|
|
1534
|
+
const next = await repositoryReadinessFor(connection);
|
|
1535
|
+
if (next.state !== cached.readiness.state) {
|
|
1536
|
+
if (next.state === 'ready') {
|
|
1537
|
+
log(`[${new Date().toISOString()}] Repository ready for ${connection.connectionId}; resuming repository-aware polling.`);
|
|
1538
|
+
}
|
|
1539
|
+
else {
|
|
1540
|
+
error(options.safeStartupDiagnostics
|
|
1541
|
+
? `[${new Date().toISOString()}] Repository not ready for ${connection.connectionId}.`
|
|
1542
|
+
: `[${new Date().toISOString()}] Repository not ready for ${connection.connectionId}: ${next.reason}`);
|
|
1543
|
+
}
|
|
987
1544
|
}
|
|
1545
|
+
cached = { readiness: next, checkedAt: readinessNow };
|
|
1546
|
+
repositoryReadiness.set(connection.connectionId, cached);
|
|
988
1547
|
}
|
|
989
|
-
cached
|
|
990
|
-
|
|
991
|
-
}
|
|
992
|
-
if (cached.readiness.state === 'ready')
|
|
993
|
-
readyConnections.push(connection);
|
|
994
|
-
}
|
|
995
|
-
const readinessOutcome = await reportLoopReadiness(client, buildReadinessReport(pollableConnections, options.unavailableConnections, repositoryReadiness), readinessReporting, { error });
|
|
996
|
-
for (const connectionId of [...skippedConnections])
|
|
997
|
-
skipConnection(connectionId);
|
|
998
|
-
if (readinessOutcome === 'token_rejected')
|
|
999
|
-
return tokenRejected();
|
|
1000
|
-
let usageClaimLimit = options.usageLimitPause.claimLimit();
|
|
1001
|
-
const mayClaim = () => usageClaimLimit === null || usageClaimLimit > 0;
|
|
1002
|
-
const recordCompletedClaim = (usageLimitMessage) => {
|
|
1003
|
-
if (usageClaimLimit !== null)
|
|
1004
|
-
usageClaimLimit--;
|
|
1005
|
-
if (usageLimitMessage) {
|
|
1006
|
-
options.usageLimitPause.pause(usageLimitMessage);
|
|
1007
|
-
usageClaimLimit = 0;
|
|
1548
|
+
if (cached.readiness.state === 'ready')
|
|
1549
|
+
readyConnections.push(connection);
|
|
1008
1550
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
if (
|
|
1551
|
+
const repositoryPollableConnections = readyConnections;
|
|
1552
|
+
const readinessOutcome = await reportLoopReadiness(client, buildReadinessReport(temporaryRepositories
|
|
1553
|
+
? pairing.connections.filter((connection) => !skippedConnections.has(connection.connectionId))
|
|
1554
|
+
: pollableConnections, options.unavailableConnections, repositoryReadiness), readinessReporting, { error });
|
|
1555
|
+
for (const connectionId of [...skippedConnections])
|
|
1556
|
+
skipConnection(connectionId);
|
|
1557
|
+
if (readinessOutcome === 'token_rejected')
|
|
1558
|
+
return tokenRejected();
|
|
1559
|
+
usageClaimLimit = options.usageLimitPause.claimLimit();
|
|
1560
|
+
if (!options.shouldStop() && wholeReadBuildPollingEnabled) {
|
|
1016
1561
|
try {
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1562
|
+
const { assignments } = await client.listBuildAssignments();
|
|
1563
|
+
void sharedSources.reconcile(assignments.map((assignment) => wholeReadSharedSourceKey(pairing, assignment))).catch((reason) => {
|
|
1564
|
+
error('[whole-read-temp] cleanup_failed:', reason instanceof Error ? reason.message : reason);
|
|
1565
|
+
});
|
|
1566
|
+
if (wholeReadPool.poll(assignments) > 0)
|
|
1019
1567
|
claimedAny = true;
|
|
1020
|
-
recordCompletedClaim(await runOneSurfaceMapping(options.anthropic, client, connection, runtimes.get(connection.connectionId), mapping.job, { log, error }));
|
|
1021
|
-
}
|
|
1022
1568
|
}
|
|
1023
1569
|
catch (reason) {
|
|
1024
1570
|
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1025
1571
|
return tokenRejected();
|
|
1026
|
-
if (
|
|
1027
|
-
|
|
1028
|
-
continue;
|
|
1029
|
-
}
|
|
1030
|
-
const status = surfaceMappingUnavailableStatus(reason);
|
|
1031
|
-
if (status !== null) {
|
|
1032
|
-
error(`Automatic mapping is not available on this server (HTTP ${status}); continuing without it.`);
|
|
1033
|
-
surfaceMappingPollingEnabled = false;
|
|
1572
|
+
if (reason instanceof CompanionHttpError && [403, 404, 405].includes(reason.status)) {
|
|
1573
|
+
wholeReadBuildPollingEnabled = false;
|
|
1034
1574
|
}
|
|
1035
1575
|
else {
|
|
1036
|
-
error(
|
|
1576
|
+
error('Whole-read build poll failed:', reason instanceof Error ? reason.message : reason);
|
|
1037
1577
|
}
|
|
1038
1578
|
}
|
|
1039
1579
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
if (!claim.task)
|
|
1045
|
-
continue;
|
|
1046
|
-
claimedAny = true;
|
|
1047
|
-
recordCompletedClaim(await runOneTask(runtimes.get(connection.connectionId), client, connection.repoPath, claim.task, connection, { log, error }));
|
|
1048
|
-
}
|
|
1049
|
-
catch (reason) {
|
|
1050
|
-
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1051
|
-
return tokenRejected();
|
|
1052
|
-
if (isUnknownConnectionError(reason)) {
|
|
1053
|
-
skipConnection(connection.connectionId);
|
|
1580
|
+
for (const connection of repositoryPollableConnections) {
|
|
1581
|
+
if (options.shouldStop() || !mayClaim())
|
|
1582
|
+
break;
|
|
1583
|
+
if (skippedConnections.has(connection.connectionId))
|
|
1054
1584
|
continue;
|
|
1585
|
+
if (surfaceMappingPollingEnabled) {
|
|
1586
|
+
try {
|
|
1587
|
+
const mapping = await client.claimNextSurfaceMapping(connection.connectionId);
|
|
1588
|
+
if (mapping.job) {
|
|
1589
|
+
claimedAny = true;
|
|
1590
|
+
recordCompletedClaim(await runOneSurfaceMapping(options.anthropic, client, connection, runtimes.get(connection.connectionId), mapping.job, { log, error }, repositoryProvider));
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
catch (reason) {
|
|
1594
|
+
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1595
|
+
return tokenRejected();
|
|
1596
|
+
if (isUnknownConnectionError(reason)) {
|
|
1597
|
+
skipConnection(connection.connectionId);
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
const status = surfaceMappingUnavailableStatus(reason);
|
|
1601
|
+
if (status !== null) {
|
|
1602
|
+
error(`Automatic mapping is not available on this server (HTTP ${status}); continuing without it.`);
|
|
1603
|
+
surfaceMappingPollingEnabled = false;
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
error(`Surface-mapping poll failed for ${connection.connectionId}:`, reason instanceof Error ? reason.message : reason);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1055
1609
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
if (claim.task) {
|
|
1610
|
+
if (!mayClaim() || skippedConnections.has(connection.connectionId))
|
|
1611
|
+
continue;
|
|
1612
|
+
try {
|
|
1613
|
+
const claim = await client.claimNext(connection.connectionId);
|
|
1614
|
+
if (!claim.task)
|
|
1615
|
+
continue;
|
|
1063
1616
|
claimedAny = true;
|
|
1064
|
-
recordCompletedClaim(await runOneTask(
|
|
1617
|
+
recordCompletedClaim(await runOneTask(runtimes.get(connection.connectionId), client, connection.repoPath, claim.task, connection, { log, error }, repositoryProvider));
|
|
1618
|
+
}
|
|
1619
|
+
catch (reason) {
|
|
1620
|
+
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1621
|
+
return tokenRejected();
|
|
1622
|
+
if (isUnknownConnectionError(reason)) {
|
|
1623
|
+
skipConnection(connection.connectionId);
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
error(`Poll failed for ${connection.connectionId}:`, reason instanceof Error ? reason.message : reason);
|
|
1065
1627
|
}
|
|
1066
1628
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
const investigation = await client.claimNextInvestigation();
|
|
1080
|
-
({ consecutiveFailures: investigationPollFailures } = nextInvestigationPollBackoff(investigationPollFailures, true));
|
|
1081
|
-
nextInvestigationPollAt = 0;
|
|
1082
|
-
if (investigation) {
|
|
1083
|
-
claimedAny = true;
|
|
1084
|
-
recordCompletedClaim(await runOneInvestigation(options.anthropic, client, pollableConnections, runtimes, investigation, { log, error }));
|
|
1629
|
+
if (!options.shouldStop() && workspaceRuntime && mayClaim()) {
|
|
1630
|
+
try {
|
|
1631
|
+
const claim = await client.claimNext();
|
|
1632
|
+
if (claim.task) {
|
|
1633
|
+
claimedAny = true;
|
|
1634
|
+
recordCompletedClaim(await runOneTask(workspaceRuntime, client, process.cwd(), claim.task, undefined, { log, error }));
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
catch (reason) {
|
|
1638
|
+
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1639
|
+
return tokenRejected();
|
|
1640
|
+
error('Workspace-level poll failed:', reason instanceof Error ? reason.message : reason);
|
|
1085
1641
|
}
|
|
1086
1642
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1643
|
+
const remainingConfigured = pairing.connections.filter((connection) => !skippedConnections.has(connection.connectionId));
|
|
1644
|
+
if (!options.shouldStop() && mayClaim() && repositoryPollableConnections.length > 0
|
|
1645
|
+
&& repositoryPollableConnections.length === pollableConnections.length
|
|
1646
|
+
&& pollableConnections.length === remainingConfigured.length
|
|
1647
|
+
&& Date.now() >= nextInvestigationPollAt) {
|
|
1648
|
+
try {
|
|
1649
|
+
const investigation = await client.claimNextInvestigation();
|
|
1650
|
+
({ consecutiveFailures: investigationPollFailures } = nextInvestigationPollBackoff(investigationPollFailures, true));
|
|
1651
|
+
nextInvestigationPollAt = 0;
|
|
1652
|
+
if (investigation) {
|
|
1653
|
+
claimedAny = true;
|
|
1654
|
+
recordCompletedClaim(await runOneInvestigation(options.anthropic, client, pollableConnections, runtimes, investigation, { log, error }, repositoryProvider));
|
|
1655
|
+
}
|
|
1095
1656
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1657
|
+
catch (reason) {
|
|
1658
|
+
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1659
|
+
return tokenRejected();
|
|
1660
|
+
const backoff = nextInvestigationPollBackoff(investigationPollFailures, false);
|
|
1661
|
+
investigationPollFailures = backoff.consecutiveFailures;
|
|
1662
|
+
nextInvestigationPollAt = Date.now() + backoff.delayMs;
|
|
1663
|
+
if (investigationPollFailures === 1) {
|
|
1664
|
+
error('Investigation poll failed:', reason instanceof Error ? reason.message : reason, `Retrying in ${backoff.delayMs / 1000}s.`);
|
|
1665
|
+
}
|
|
1666
|
+
else {
|
|
1667
|
+
warn(`Investigation poll still failing (${investigationPollFailures} consecutive); retrying in ${backoff.delayMs / 1000}s.`);
|
|
1668
|
+
}
|
|
1098
1669
|
}
|
|
1099
1670
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1671
|
+
if (claimedAny) {
|
|
1672
|
+
lastIdleStatusAt = Date.now();
|
|
1673
|
+
}
|
|
1674
|
+
else if (!options.shouldStop()) {
|
|
1675
|
+
const now = Date.now();
|
|
1676
|
+
if (now - lastIdleStatusAt >= IDLE_STATUS_MS) {
|
|
1677
|
+
log(`[${new Date(now).toISOString()}] still watching — no work claimed.`);
|
|
1678
|
+
lastIdleStatusAt = now;
|
|
1679
|
+
}
|
|
1680
|
+
await new Promise((done) => setTimeout(done, IDLE_POLL_MS));
|
|
1109
1681
|
}
|
|
1110
|
-
await new Promise((done) => setTimeout(done, IDLE_POLL_MS));
|
|
1111
1682
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1683
|
+
catch (reason) {
|
|
1684
|
+
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1685
|
+
return tokenRejected();
|
|
1686
|
+
error('Unexpected run-loop error:', reason instanceof Error ? reason.message : reason);
|
|
1687
|
+
if (!options.shouldStop())
|
|
1688
|
+
await new Promise((done) => setTimeout(done, LOOP_RECOVERY_DELAY_MS));
|
|
1689
|
+
}
|
|
1119
1690
|
}
|
|
1120
1691
|
}
|
|
1692
|
+
finally {
|
|
1693
|
+
await wholeReadPool.shutdown();
|
|
1694
|
+
await sharedSources.shutdown();
|
|
1695
|
+
}
|
|
1121
1696
|
log('Stopped.');
|
|
1122
1697
|
return 'stopped';
|
|
1123
1698
|
}
|
|
1124
|
-
async function
|
|
1699
|
+
async function runCompanionLoop(options = {}) {
|
|
1125
1700
|
let config;
|
|
1126
1701
|
try {
|
|
1127
1702
|
config = loadConfig();
|
|
@@ -1271,6 +1846,22 @@ async function cmdRun(options = {}) {
|
|
|
1271
1846
|
}
|
|
1272
1847
|
}));
|
|
1273
1848
|
}
|
|
1849
|
+
async function cmdRun(options = {}) {
|
|
1850
|
+
// Cleanup is deliberately not flag-gated: source left by an older enabled build must still be
|
|
1851
|
+
// removed after rollback or while its Console is offline. Importing the module starts no timer.
|
|
1852
|
+
try {
|
|
1853
|
+
await startAttemptSourceSweeper();
|
|
1854
|
+
}
|
|
1855
|
+
catch {
|
|
1856
|
+
console.error('[whole-read-temp] cleanup_sweep_failed');
|
|
1857
|
+
}
|
|
1858
|
+
try {
|
|
1859
|
+
await runCompanionLoop(options);
|
|
1860
|
+
}
|
|
1861
|
+
finally {
|
|
1862
|
+
stopAttemptSourceSweeper();
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1274
1865
|
async function cmdRunWithInstance(options = {}) {
|
|
1275
1866
|
if (process.env.PRODUCT_CONSOLE_COMPANION_DAEMON_CHILD === '1')
|
|
1276
1867
|
return cmdRun(options);
|
|
@@ -1393,9 +1984,8 @@ async function cmdDisconnect(argv) {
|
|
|
1393
1984
|
await removeManagedCopies(removed.flatMap((pairing) => pairing.connections));
|
|
1394
1985
|
logRestartHintIfRunning();
|
|
1395
1986
|
}
|
|
1396
|
-
/**
|
|
1397
|
-
*
|
|
1398
|
-
* <config dir>/repos/ are ever deleted, and only for connections marked managed. */
|
|
1987
|
+
/** Legacy disconnect behavior. B38-c owns deletion for migrated storage; until then those paths
|
|
1988
|
+
* are left untouched. A legacy copy another remaining connection uses is also kept. */
|
|
1399
1989
|
async function removeManagedCopies(removedConnections) {
|
|
1400
1990
|
const reposDir = managedReposDir();
|
|
1401
1991
|
const stillUsed = new Set((loadConfig()?.pairings ?? [])
|
|
@@ -1403,7 +1993,7 @@ async function removeManagedCopies(removedConnections) {
|
|
|
1403
1993
|
.map((connection) => resolve(connection.repoPath).toLowerCase()));
|
|
1404
1994
|
const handled = new Set();
|
|
1405
1995
|
for (const connection of removedConnections) {
|
|
1406
|
-
if (!connection.managed)
|
|
1996
|
+
if (!connection.managed || usesTemporaryManagedStorage(connection))
|
|
1407
1997
|
continue;
|
|
1408
1998
|
const key = resolve(connection.repoPath).toLowerCase();
|
|
1409
1999
|
if (stillUsed.has(key) || handled.has(key))
|
|
@@ -1427,7 +2017,7 @@ async function cmdSignin(argv) {
|
|
|
1427
2017
|
let wanted = null;
|
|
1428
2018
|
if (flags.repo !== undefined) {
|
|
1429
2019
|
try {
|
|
1430
|
-
wanted =
|
|
2020
|
+
wanted = githubLabel(parseGitHubRepo(flags.repo)).toLowerCase();
|
|
1431
2021
|
}
|
|
1432
2022
|
catch (error) {
|
|
1433
2023
|
console.error(error instanceof ManagedRepoError ? error.message : String(error));
|
|
@@ -1435,24 +2025,39 @@ async function cmdSignin(argv) {
|
|
|
1435
2025
|
return;
|
|
1436
2026
|
}
|
|
1437
2027
|
}
|
|
1438
|
-
const
|
|
2028
|
+
const targets = new Map();
|
|
1439
2029
|
for (const pairing of loadConfig()?.pairings ?? []) {
|
|
1440
2030
|
const client = new CompanionClient(pairing.url, pairing.token);
|
|
1441
2031
|
for (const connection of pairing.connections) {
|
|
1442
2032
|
if (!connection.managed)
|
|
1443
2033
|
continue;
|
|
1444
|
-
|
|
1445
|
-
|
|
2034
|
+
let repository;
|
|
2035
|
+
try {
|
|
2036
|
+
repository = configuredManagedRepository(connection);
|
|
2037
|
+
}
|
|
2038
|
+
catch {
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
2041
|
+
const label = githubLabel(repository);
|
|
2042
|
+
const key = `${normalizeConsoleUrl(pairing.url)}\0${pairing.workspaceId ?? ''}\0${label.toLowerCase()}`;
|
|
2043
|
+
if (targets.has(key) || (wanted !== null && label.toLowerCase() !== wanted))
|
|
1446
2044
|
continue;
|
|
1447
|
-
|
|
1448
|
-
|
|
2045
|
+
targets.set(key, usesTemporaryManagedStorage(connection) ? {
|
|
2046
|
+
storage: 'temporary',
|
|
2047
|
+
repository,
|
|
2048
|
+
label,
|
|
2049
|
+
branch: connection.branch,
|
|
2050
|
+
getToken: () => client.githubToken(label).then((response) => response.token),
|
|
2051
|
+
} : {
|
|
2052
|
+
storage: 'persistent',
|
|
2053
|
+
repository,
|
|
1449
2054
|
repoPath: connection.repoPath,
|
|
1450
2055
|
label,
|
|
1451
2056
|
getToken: () => client.githubToken(label).then((response) => response.token),
|
|
1452
2057
|
});
|
|
1453
2058
|
}
|
|
1454
2059
|
}
|
|
1455
|
-
if (
|
|
2060
|
+
if (targets.size === 0) {
|
|
1456
2061
|
console.log(wanted === null
|
|
1457
2062
|
? "There's no companion-kept copy of any repo on this computer."
|
|
1458
2063
|
: `There's no companion-kept copy of ${flags.repo} on this computer.`);
|
|
@@ -1460,7 +2065,24 @@ async function cmdSignin(argv) {
|
|
|
1460
2065
|
process.exitCode = 1;
|
|
1461
2066
|
return;
|
|
1462
2067
|
}
|
|
1463
|
-
for (const
|
|
2068
|
+
for (const target of targets.values()) {
|
|
2069
|
+
const { label, getToken } = target;
|
|
2070
|
+
if (target.storage === 'temporary') {
|
|
2071
|
+
try {
|
|
2072
|
+
await proveManagedRepositoryAccess({
|
|
2073
|
+
repository: target.repository,
|
|
2074
|
+
branch: target.branch,
|
|
2075
|
+
credential: await getToken(),
|
|
2076
|
+
}, { managedRepositoryUrl: (repository) => testCloneUrl(repository) ?? githubCloneUrl(repository) });
|
|
2077
|
+
console.log(`GitHub access is ready for ${label}.`);
|
|
2078
|
+
}
|
|
2079
|
+
catch {
|
|
2080
|
+
console.log(`Still can't reach ${label} on GitHub. Reconnect GitHub in the Console.`);
|
|
2081
|
+
process.exitCode = 1;
|
|
2082
|
+
}
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
const { repoPath } = target;
|
|
1464
2086
|
if (!existsSync(repoPath)) {
|
|
1465
2087
|
console.log(`The companion's copy of ${label} is missing. Run the connect command for it again.`);
|
|
1466
2088
|
process.exitCode = 1;
|
|
@@ -1496,6 +2118,11 @@ function cmdStatus() {
|
|
|
1496
2118
|
const workspace = pairing.workspaceId ? ` (workspace ${pairing.workspaceId.slice(0, 8)}…)` : '';
|
|
1497
2119
|
console.log(` ${pairing.url}${workspace}`);
|
|
1498
2120
|
for (const project of pairing.projects) {
|
|
2121
|
+
if (project.managed && project.repositoryStorage === 'temporary') {
|
|
2122
|
+
const label = project.repository?.path ?? project.label ?? labelFromCopyPath(project.repoPath);
|
|
2123
|
+
console.log(` ${project.connectionId} — ${label} — temporary repository access`);
|
|
2124
|
+
continue;
|
|
2125
|
+
}
|
|
1499
2126
|
console.log(` ${project.connectionId} — ${project.repoPath}`);
|
|
1500
2127
|
if (!project.managed)
|
|
1501
2128
|
continue;
|