@withtruing/companion 0.13.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -26
- 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 +837 -207
- package/dist/codex-binary.js +96 -0
- package/dist/codex-cli-caller.js +90 -16
- package/dist/config.js +177 -2
- package/dist/git-range.js +88 -46
- package/dist/http-client.js +95 -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, 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
|
});
|
|
@@ -461,7 +543,7 @@ export function codexReadinessLines(status) {
|
|
|
461
543
|
}
|
|
462
544
|
return [
|
|
463
545
|
'Codex needs a one-time login for the companion\'s own home:',
|
|
464
|
-
' Windows PowerShell: New-Item -ItemType Directory -Force "$HOME\\.product-console-companion\\codex-home" | Out-Null; $env:CODEX_HOME="$HOME\\.product-console-companion\\codex-home"; codex login; Remove-Item Env:CODEX_HOME',
|
|
546
|
+
' Windows PowerShell: New-Item -ItemType Directory -Force "$HOME\\.product-console-companion\\codex-home" | Out-Null; $env:CODEX_HOME="$HOME\\.product-console-companion\\codex-home"; codex.cmd login; Remove-Item Env:CODEX_HOME',
|
|
465
547
|
' macOS/Linux: mkdir -p "$HOME/.product-console-companion/codex-home" && CODEX_HOME="$HOME/.product-console-companion/codex-home" codex login',
|
|
466
548
|
'This separate login keeps your own ~/.codex instructions out of the companion\'s calls.',
|
|
467
549
|
];
|
|
@@ -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,223 @@ 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
|
+
if (runtime.backend === 'api-key')
|
|
1184
|
+
return 'anthropic_api';
|
|
1185
|
+
if (runtime.backend === 'cli')
|
|
1186
|
+
return 'claude_cli';
|
|
1187
|
+
return 'codex';
|
|
1188
|
+
}
|
|
1189
|
+
async function runWholeReadAssignment(input) {
|
|
1190
|
+
const connection = input.connections.find((candidate) => candidate.connectionId === input.assignment.connectionId);
|
|
1191
|
+
const local = connection && !connection.managed ? connection : undefined;
|
|
1192
|
+
const runtime = [
|
|
1193
|
+
connection ? input.runtimes.get(connection.connectionId) : undefined,
|
|
1194
|
+
input.workspaceRuntime ?? undefined,
|
|
1195
|
+
...input.runtimes.values(),
|
|
1196
|
+
].find((candidate) => candidate !== undefined
|
|
1197
|
+
&& wholeReadBackendName(candidate) === input.assignment.frozenBackend);
|
|
1198
|
+
const backendReady = runtime !== undefined
|
|
1199
|
+
&& wholeReadBackendName(runtime) === input.assignment.frozenBackend;
|
|
1200
|
+
const routeReadiness = assignmentRouteReadinessCode(input.assignment.route);
|
|
1201
|
+
if (routeReadiness !== 'ready') {
|
|
1202
|
+
await input.client.acceptBuildAssignment(input.assignment, {
|
|
1203
|
+
lsRemoteVerified: false, exactCommitFetched: false,
|
|
1204
|
+
backend: input.assignment.frozenBackend, backendReady,
|
|
1205
|
+
routeSupported: false,
|
|
1206
|
+
});
|
|
1207
|
+
return { status: 'not_claimed' };
|
|
1208
|
+
}
|
|
1209
|
+
if (input.assignment.state === 'offered') {
|
|
1210
|
+
let verified = false;
|
|
1211
|
+
try {
|
|
1212
|
+
const acquired = await input.sharedSources.acquire(wholeReadSharedSourceKey(input.pairing, input.assignment), async (lease, signal) => {
|
|
1213
|
+
const source = await acquireAssignedSource({
|
|
1214
|
+
route: input.assignment.route,
|
|
1215
|
+
acquireLocal: () => local?.branch
|
|
1216
|
+
? acquireLocalSource({ repoPath: local.repoPath, branch: local.branch,
|
|
1217
|
+
snapshotSha: input.assignment.snapshotSha, signal })
|
|
1218
|
+
: Promise.reject(new WholeReadGitError('snapshot_unavailable')),
|
|
1219
|
+
acquireGithubCompanion: () => {
|
|
1220
|
+
const repositoryUrl = `https://github.com/${githubLabel(parseGitHubRepo(input.assignment.canonicalRepository))}.git`;
|
|
1221
|
+
return acquireGithubCompanionSource({ lease, repositoryUrl,
|
|
1222
|
+
snapshotSha: input.assignment.snapshotSha, signal });
|
|
1223
|
+
},
|
|
1224
|
+
});
|
|
1225
|
+
if (!source)
|
|
1226
|
+
throw new WholeReadGitError('snapshot_unavailable');
|
|
1227
|
+
return source;
|
|
1228
|
+
});
|
|
1229
|
+
await acquired.release();
|
|
1230
|
+
verified = true;
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
verified = false;
|
|
1234
|
+
}
|
|
1235
|
+
const acceptance = await input.client.acceptBuildAssignment(input.assignment, {
|
|
1236
|
+
lsRemoteVerified: verified, exactCommitFetched: verified,
|
|
1237
|
+
backend: input.assignment.frozenBackend, backendReady,
|
|
1238
|
+
routeSupported: routeReadiness === 'ready',
|
|
1239
|
+
});
|
|
1240
|
+
if (acceptance.status !== 'ready')
|
|
1241
|
+
return { status: 'not_claimed' };
|
|
1242
|
+
}
|
|
1243
|
+
if (!runtime || !backendReady || input.shouldStop())
|
|
1244
|
+
return { status: 'not_claimed' };
|
|
1245
|
+
const claimed = await input.client.claimBuildUnit(input.assignment, {
|
|
1246
|
+
backend: input.assignment.frozenBackend, backendReady: true,
|
|
1247
|
+
payerIdentities: input.assignment.payerIdentity ? [input.assignment.payerIdentity] : [],
|
|
1248
|
+
});
|
|
1249
|
+
if (claimed.status !== 'claimed')
|
|
1250
|
+
return {
|
|
1251
|
+
status: claimed.status === 'build_concurrency_limit' ? 'build_concurrency_limit' : 'not_claimed',
|
|
1252
|
+
};
|
|
1253
|
+
const claim = claimed;
|
|
1254
|
+
const controller = new AbortController();
|
|
1255
|
+
if (claim.kind === 'group' || claim.kind === 'prd') {
|
|
1256
|
+
try {
|
|
1257
|
+
await withAttemptSource({
|
|
1258
|
+
workspaceId: input.assignment.workspaceId,
|
|
1259
|
+
buildId: input.assignment.buildId,
|
|
1260
|
+
attemptId: claim.attemptId,
|
|
1261
|
+
hostId: input.pairing.workspaceId ?? 'companion',
|
|
1262
|
+
leaseExpiresAt: claim.leaseExpiresAt,
|
|
1263
|
+
}, async (lease) => dispatchWholeReadClaim(claim, {
|
|
1264
|
+
backend: wholeReadBackend(runtime, {
|
|
1265
|
+
attemptDirectory: lease.path,
|
|
1266
|
+
signal: controller.signal,
|
|
1267
|
+
registerReader: (reader) => lease.registerReader(reader),
|
|
1268
|
+
}),
|
|
1269
|
+
transport: wholeReadTransport(input.client, input.onUsageLimit),
|
|
1270
|
+
signal: controller.signal,
|
|
1271
|
+
}));
|
|
1272
|
+
return { status: 'claimed' };
|
|
1273
|
+
}
|
|
1274
|
+
catch (reason) {
|
|
1275
|
+
if (reason instanceof AttemptSourceCleanupError) {
|
|
1276
|
+
await input.client.reportBuildError(claim, { code: 'cleanup_pending', source: 'companion' });
|
|
1277
|
+
return { status: 'claimed' };
|
|
1278
|
+
}
|
|
1279
|
+
await input.client.reportBuildError(claim, {
|
|
1280
|
+
code: 'capability_failure', source: wholeReadBackendName(runtime),
|
|
1281
|
+
});
|
|
1282
|
+
return { status: 'claimed' };
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
try {
|
|
1286
|
+
await withAttemptSource({
|
|
1287
|
+
workspaceId: input.assignment.workspaceId,
|
|
1288
|
+
buildId: input.assignment.buildId,
|
|
1289
|
+
attemptId: claim.attemptId,
|
|
1290
|
+
hostId: input.pairing.workspaceId ?? 'companion',
|
|
1291
|
+
leaseExpiresAt: claim.leaseExpiresAt,
|
|
1292
|
+
}, async (lease) => {
|
|
1293
|
+
const shared = await input.sharedSources.acquire(wholeReadSharedSourceKey(input.pairing, input.assignment), async (sourceLease, signal) => {
|
|
1294
|
+
const source = await acquireAssignedSource({
|
|
1295
|
+
route: input.assignment.route,
|
|
1296
|
+
acquireLocal: () => local?.branch
|
|
1297
|
+
? acquireLocalSource({ repoPath: local.repoPath, branch: local.branch,
|
|
1298
|
+
snapshotSha: input.assignment.snapshotSha, signal })
|
|
1299
|
+
: Promise.reject(new WholeReadGitError('snapshot_unavailable')),
|
|
1300
|
+
acquireGithubCompanion: async () => {
|
|
1301
|
+
const repositoryUrl = `https://github.com/${githubLabel(parseGitHubRepo(input.assignment.canonicalRepository))}.git`;
|
|
1302
|
+
return acquireGithubCompanionSource({ lease: sourceLease, repositoryUrl,
|
|
1303
|
+
snapshotSha: input.assignment.snapshotSha, signal });
|
|
1304
|
+
},
|
|
1305
|
+
});
|
|
1306
|
+
if (!source)
|
|
1307
|
+
throw new WholeReadGitError('snapshot_unavailable');
|
|
1308
|
+
return source;
|
|
1309
|
+
}, controller.signal);
|
|
1310
|
+
try {
|
|
1311
|
+
await dispatchWholeReadClaim(claim, { source: shared.source,
|
|
1312
|
+
backend: wholeReadBackend(runtime, {
|
|
1313
|
+
attemptDirectory: lease.path,
|
|
1314
|
+
signal: controller.signal,
|
|
1315
|
+
registerReader: (reader) => lease.registerReader(reader),
|
|
1316
|
+
}),
|
|
1317
|
+
transport: wholeReadTransport(input.client, input.onUsageLimit), signal: controller.signal });
|
|
1318
|
+
}
|
|
1319
|
+
finally {
|
|
1320
|
+
await shared.release();
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
return { status: 'claimed' };
|
|
1324
|
+
}
|
|
1325
|
+
catch (reason) {
|
|
1326
|
+
if (reason instanceof AttemptSourceCleanupError) {
|
|
1327
|
+
await input.client.reportBuildError(claim, { code: 'cleanup_pending', source: 'companion' });
|
|
1328
|
+
}
|
|
1329
|
+
else if (reason instanceof WholeReadGitError || (reason instanceof Error
|
|
1330
|
+
&& reason.message === 'snapshot_unavailable')) {
|
|
1331
|
+
await input.client.reportBuildError(claim, { code: 'snapshot_unavailable', source: 'companion' });
|
|
1332
|
+
}
|
|
1333
|
+
else {
|
|
1334
|
+
await input.client.reportBuildError(claim, {
|
|
1335
|
+
code: 'capability_failure', source: wholeReadBackendName(runtime),
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
return { status: 'claimed' };
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
function wholeReadSharedSourceKey(pairing, assignment) {
|
|
1342
|
+
return {
|
|
1343
|
+
workspaceId: assignment.workspaceId,
|
|
1344
|
+
buildId: assignment.buildId,
|
|
1345
|
+
assignmentId: assignment.id,
|
|
1346
|
+
assignmentRevision: assignment.assignmentRevision,
|
|
1347
|
+
snapshotSha: assignment.snapshotSha,
|
|
1348
|
+
hostId: pairing.workspaceId ?? 'companion',
|
|
1349
|
+
};
|
|
853
1350
|
}
|
|
854
1351
|
function pairingPrefix(pairing) {
|
|
855
1352
|
try {
|
|
@@ -864,16 +1361,24 @@ async function runPairing(pairing, options) {
|
|
|
864
1361
|
const log = (...values) => console.log(prefix + String(values[0] ?? ''), ...values.slice(1));
|
|
865
1362
|
const error = (...values) => console.error(prefix + String(values[0] ?? ''), ...values.slice(1));
|
|
866
1363
|
const warn = (...values) => console.warn(prefix + String(values[0] ?? ''), ...values.slice(1));
|
|
867
|
-
const
|
|
1364
|
+
const fastModelsByConnection = Object.fromEntries(pairing.connections.flatMap((connection) => {
|
|
1365
|
+
if (connection.backend === 'cli')
|
|
1366
|
+
return [[connection.connectionId, CLAUDE_CLI_FAST_MODEL]];
|
|
1367
|
+
if (connection.backend === 'codex')
|
|
1368
|
+
return [[connection.connectionId, CODEX_FAST_MODEL]];
|
|
1369
|
+
return [];
|
|
1370
|
+
}));
|
|
1371
|
+
const client = new CompanionClient(pairing.url, pairing.token, fastModelsByConnection);
|
|
868
1372
|
const tokenRejected = () => {
|
|
869
1373
|
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
1374
|
return 'token_rejected';
|
|
871
1375
|
};
|
|
1376
|
+
let serverIdentity;
|
|
872
1377
|
if (options.multiple) {
|
|
873
1378
|
let failures = 0;
|
|
874
1379
|
while (!options.shouldStop()) {
|
|
875
1380
|
try {
|
|
876
|
-
await client.whoami();
|
|
1381
|
+
serverIdentity = await client.whoami();
|
|
877
1382
|
break;
|
|
878
1383
|
}
|
|
879
1384
|
catch (reason) {
|
|
@@ -889,13 +1394,23 @@ async function runPairing(pairing, options) {
|
|
|
889
1394
|
return 'stopped';
|
|
890
1395
|
}
|
|
891
1396
|
else {
|
|
892
|
-
await client.whoami();
|
|
1397
|
+
serverIdentity = await client.whoami();
|
|
1398
|
+
}
|
|
1399
|
+
const temporaryRepositories = serverIdentity.temporaryRepositories === true;
|
|
1400
|
+
if (temporaryRepositories) {
|
|
1401
|
+
const migrated = migrateManagedRepositoriesToTemporaryStorage({
|
|
1402
|
+
url: pairing.url,
|
|
1403
|
+
workspaceId: serverIdentity.workspaceId,
|
|
1404
|
+
});
|
|
1405
|
+
if (migrated)
|
|
1406
|
+
pairing.connections = migrated.connections;
|
|
1407
|
+
await activateManagedCopyCleanup(pairing.url, serverIdentity.workspaceId, { log, error });
|
|
893
1408
|
}
|
|
894
1409
|
const workspaceBackend = pairing.workspaceBackend ?? 'api-key';
|
|
895
1410
|
const activeConnections = pairing.connections.filter((connection) => !options.unavailableConnections.has(connection.connectionId));
|
|
896
1411
|
for (const connection of activeConnections) {
|
|
897
|
-
if (connection.managed) {
|
|
898
|
-
const label =
|
|
1412
|
+
if (connection.managed && !temporaryRepositories && !usesTemporaryManagedStorage(connection)) {
|
|
1413
|
+
const label = githubLabel(configuredManagedRepository(connection));
|
|
899
1414
|
watchManagedCopy(connection.repoPath, label, (message) => error(`[${new Date().toISOString()}] ${message}`), () => client.githubToken(label));
|
|
900
1415
|
}
|
|
901
1416
|
}
|
|
@@ -903,13 +1418,18 @@ async function runPairing(pairing, options) {
|
|
|
903
1418
|
connection.connectionId,
|
|
904
1419
|
createBackendRuntime(connection, options.anthropic),
|
|
905
1420
|
]));
|
|
1421
|
+
const repositoryProvider = ordinaryTaskRepositoryProvider(pairing, client);
|
|
1422
|
+
const repositoryReadinessFor = (connection) => checkConnectionRepositoryReadiness(connection, temporaryRepositories, {
|
|
1423
|
+
getCredential: async (_configured, repository) => (await client.githubToken(githubLabel(repository))).token,
|
|
1424
|
+
managedRepositoryUrl: (repository) => testCloneUrl(repository) ?? githubCloneUrl(repository),
|
|
1425
|
+
});
|
|
906
1426
|
const workspaceRuntime = pairing.pollWorkspaceTasks && !options.workspaceTasksUnavailable
|
|
907
1427
|
? createBackendRuntime({ backend: workspaceBackend, codexModel: pairing.codexModel }, options.anthropic)
|
|
908
1428
|
: null;
|
|
909
1429
|
const repositoryReadiness = new Map();
|
|
910
1430
|
for (const connection of activeConnections) {
|
|
911
1431
|
repositoryReadiness.set(connection.connectionId, {
|
|
912
|
-
readiness: await
|
|
1432
|
+
readiness: await repositoryReadinessFor(connection),
|
|
913
1433
|
checkedAt: Date.now(),
|
|
914
1434
|
});
|
|
915
1435
|
}
|
|
@@ -924,6 +1444,10 @@ async function runPairing(pairing, options) {
|
|
|
924
1444
|
for (const connection of activeConnections) {
|
|
925
1445
|
const readiness = repositoryReadiness.get(connection.connectionId).readiness;
|
|
926
1446
|
if (readiness.state === 'ready') {
|
|
1447
|
+
if (temporaryRepositories && connection.managed) {
|
|
1448
|
+
log(` - ${connection.connectionId} (${githubLabel(configuredManagedRepository(connection))}; temporary repository access ready) (${connection.backend})`);
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
927
1451
|
const sha = await currentCommit(connection.repoPath).catch(() => '(unknown)');
|
|
928
1452
|
log(options.safeStartupDiagnostics
|
|
929
1453
|
? ` - ${connection.connectionId} @ ${sha.slice(0, 7)} (${connection.backend})`
|
|
@@ -950,6 +1474,7 @@ async function runPairing(pairing, options) {
|
|
|
950
1474
|
let investigationPollFailures = 0;
|
|
951
1475
|
let nextInvestigationPollAt = 0;
|
|
952
1476
|
let surfaceMappingPollingEnabled = true;
|
|
1477
|
+
let wholeReadBuildPollingEnabled = true;
|
|
953
1478
|
const skippedConnections = new Set();
|
|
954
1479
|
const loggedSkippedConnections = new Set();
|
|
955
1480
|
const readinessReporting = {
|
|
@@ -964,164 +1489,217 @@ async function runPairing(pairing, options) {
|
|
|
964
1489
|
loggedSkippedConnections.add(connectionId);
|
|
965
1490
|
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
1491
|
};
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1492
|
+
let usageClaimLimit = 0;
|
|
1493
|
+
let asyncTokenRejected = false;
|
|
1494
|
+
const mayClaim = () => usageClaimLimit === null || usageClaimLimit > 0;
|
|
1495
|
+
const recordCompletedClaim = (usageLimitMessage) => {
|
|
1496
|
+
if (usageClaimLimit !== null)
|
|
1497
|
+
usageClaimLimit--;
|
|
1498
|
+
if (usageLimitMessage) {
|
|
1499
|
+
options.usageLimitPause.pause(usageLimitMessage);
|
|
1500
|
+
usageClaimLimit = 0;
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
const sharedSources = createWholeReadSharedSourceManager({
|
|
1504
|
+
onCleanupPending: () => { error('[whole-read-temp] cleanup_pending'); },
|
|
1505
|
+
});
|
|
1506
|
+
const wholeReadPool = createWholeReadAssignmentPool({
|
|
1507
|
+
key: (assignment) => assignment.id,
|
|
1508
|
+
mayClaim,
|
|
1509
|
+
shouldStop: options.shouldStop,
|
|
1510
|
+
onStart: () => { if (usageClaimLimit !== null)
|
|
1511
|
+
usageClaimLimit--; },
|
|
1512
|
+
run: (assignment) => runWholeReadAssignment({ pairing, client, assignment,
|
|
1513
|
+
connections: activeConnections, runtimes, workspaceRuntime, sharedSources,
|
|
1514
|
+
onUsageLimit: () => recordCompletedClaim('usage limit reached'),
|
|
1515
|
+
shouldStop: options.shouldStop }),
|
|
1516
|
+
onError: (reason) => {
|
|
1517
|
+
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1518
|
+
asyncTokenRejected = true;
|
|
1519
|
+
else
|
|
1520
|
+
error('Whole-read build attempt failed:', reason instanceof Error ? reason.message : reason);
|
|
1521
|
+
},
|
|
1522
|
+
});
|
|
1523
|
+
try {
|
|
1524
|
+
while (!options.shouldStop()) {
|
|
1525
|
+
try {
|
|
1526
|
+
if (asyncTokenRejected)
|
|
1527
|
+
return tokenRejected();
|
|
1528
|
+
let claimedAny = false;
|
|
1529
|
+
const pollableConnections = activeConnections.filter((connection) => !skippedConnections.has(connection.connectionId));
|
|
1530
|
+
const readyConnections = [];
|
|
1531
|
+
const readinessNow = Date.now();
|
|
1532
|
+
for (const connection of pollableConnections) {
|
|
1533
|
+
if (options.shouldStop())
|
|
1534
|
+
break;
|
|
1535
|
+
let cached = repositoryReadiness.get(connection.connectionId);
|
|
1536
|
+
if (readinessNow - cached.checkedAt >= REPOSITORY_RECHECK_MS) {
|
|
1537
|
+
const next = await repositoryReadinessFor(connection);
|
|
1538
|
+
if (next.state !== cached.readiness.state) {
|
|
1539
|
+
if (next.state === 'ready') {
|
|
1540
|
+
log(`[${new Date().toISOString()}] Repository ready for ${connection.connectionId}; resuming repository-aware polling.`);
|
|
1541
|
+
}
|
|
1542
|
+
else {
|
|
1543
|
+
error(options.safeStartupDiagnostics
|
|
1544
|
+
? `[${new Date().toISOString()}] Repository not ready for ${connection.connectionId}.`
|
|
1545
|
+
: `[${new Date().toISOString()}] Repository not ready for ${connection.connectionId}: ${next.reason}`);
|
|
1546
|
+
}
|
|
987
1547
|
}
|
|
1548
|
+
cached = { readiness: next, checkedAt: readinessNow };
|
|
1549
|
+
repositoryReadiness.set(connection.connectionId, cached);
|
|
988
1550
|
}
|
|
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;
|
|
1551
|
+
if (cached.readiness.state === 'ready')
|
|
1552
|
+
readyConnections.push(connection);
|
|
1008
1553
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
if (
|
|
1554
|
+
const repositoryPollableConnections = readyConnections;
|
|
1555
|
+
const readinessOutcome = await reportLoopReadiness(client, buildReadinessReport(temporaryRepositories
|
|
1556
|
+
? pairing.connections.filter((connection) => !skippedConnections.has(connection.connectionId))
|
|
1557
|
+
: pollableConnections, options.unavailableConnections, repositoryReadiness), readinessReporting, { error });
|
|
1558
|
+
for (const connectionId of [...skippedConnections])
|
|
1559
|
+
skipConnection(connectionId);
|
|
1560
|
+
if (readinessOutcome === 'token_rejected')
|
|
1561
|
+
return tokenRejected();
|
|
1562
|
+
usageClaimLimit = options.usageLimitPause.claimLimit();
|
|
1563
|
+
if (!options.shouldStop() && wholeReadBuildPollingEnabled) {
|
|
1016
1564
|
try {
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1565
|
+
const { assignments } = await client.listBuildAssignments();
|
|
1566
|
+
void sharedSources.reconcile(assignments.map((assignment) => wholeReadSharedSourceKey(pairing, assignment))).catch((reason) => {
|
|
1567
|
+
error('[whole-read-temp] cleanup_failed:', reason instanceof Error ? reason.message : reason);
|
|
1568
|
+
});
|
|
1569
|
+
if (wholeReadPool.poll(assignments) > 0)
|
|
1019
1570
|
claimedAny = true;
|
|
1020
|
-
recordCompletedClaim(await runOneSurfaceMapping(options.anthropic, client, connection, runtimes.get(connection.connectionId), mapping.job, { log, error }));
|
|
1021
|
-
}
|
|
1022
1571
|
}
|
|
1023
1572
|
catch (reason) {
|
|
1024
1573
|
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1025
1574
|
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;
|
|
1575
|
+
if (reason instanceof CompanionHttpError && [403, 404, 405].includes(reason.status)) {
|
|
1576
|
+
wholeReadBuildPollingEnabled = false;
|
|
1034
1577
|
}
|
|
1035
1578
|
else {
|
|
1036
|
-
error(
|
|
1579
|
+
error('Whole-read build poll failed:', reason instanceof Error ? reason.message : reason);
|
|
1037
1580
|
}
|
|
1038
1581
|
}
|
|
1039
1582
|
}
|
|
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);
|
|
1583
|
+
for (const connection of repositoryPollableConnections) {
|
|
1584
|
+
if (options.shouldStop() || !mayClaim())
|
|
1585
|
+
break;
|
|
1586
|
+
if (skippedConnections.has(connection.connectionId))
|
|
1054
1587
|
continue;
|
|
1588
|
+
if (surfaceMappingPollingEnabled) {
|
|
1589
|
+
try {
|
|
1590
|
+
const mapping = await client.claimNextSurfaceMapping(connection.connectionId);
|
|
1591
|
+
if (mapping.job) {
|
|
1592
|
+
claimedAny = true;
|
|
1593
|
+
recordCompletedClaim(await runOneSurfaceMapping(options.anthropic, client, connection, runtimes.get(connection.connectionId), mapping.job, { log, error }, repositoryProvider));
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
catch (reason) {
|
|
1597
|
+
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1598
|
+
return tokenRejected();
|
|
1599
|
+
if (isUnknownConnectionError(reason)) {
|
|
1600
|
+
skipConnection(connection.connectionId);
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
const status = surfaceMappingUnavailableStatus(reason);
|
|
1604
|
+
if (status !== null) {
|
|
1605
|
+
error(`Automatic mapping is not available on this server (HTTP ${status}); continuing without it.`);
|
|
1606
|
+
surfaceMappingPollingEnabled = false;
|
|
1607
|
+
}
|
|
1608
|
+
else {
|
|
1609
|
+
error(`Surface-mapping poll failed for ${connection.connectionId}:`, reason instanceof Error ? reason.message : reason);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1055
1612
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
if (claim.task) {
|
|
1613
|
+
if (!mayClaim() || skippedConnections.has(connection.connectionId))
|
|
1614
|
+
continue;
|
|
1615
|
+
try {
|
|
1616
|
+
const claim = await client.claimNext(connection.connectionId);
|
|
1617
|
+
if (!claim.task)
|
|
1618
|
+
continue;
|
|
1063
1619
|
claimedAny = true;
|
|
1064
|
-
recordCompletedClaim(await runOneTask(
|
|
1620
|
+
recordCompletedClaim(await runOneTask(runtimes.get(connection.connectionId), client, connection.repoPath, claim.task, connection, { log, error }, repositoryProvider));
|
|
1621
|
+
}
|
|
1622
|
+
catch (reason) {
|
|
1623
|
+
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1624
|
+
return tokenRejected();
|
|
1625
|
+
if (isUnknownConnectionError(reason)) {
|
|
1626
|
+
skipConnection(connection.connectionId);
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
error(`Poll failed for ${connection.connectionId}:`, reason instanceof Error ? reason.message : reason);
|
|
1065
1630
|
}
|
|
1066
1631
|
}
|
|
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 }));
|
|
1632
|
+
if (!options.shouldStop() && workspaceRuntime && mayClaim()) {
|
|
1633
|
+
try {
|
|
1634
|
+
const claim = await client.claimNext();
|
|
1635
|
+
if (claim.task) {
|
|
1636
|
+
claimedAny = true;
|
|
1637
|
+
recordCompletedClaim(await runOneTask(workspaceRuntime, client, process.cwd(), claim.task, undefined, { log, error }));
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
catch (reason) {
|
|
1641
|
+
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1642
|
+
return tokenRejected();
|
|
1643
|
+
error('Workspace-level poll failed:', reason instanceof Error ? reason.message : reason);
|
|
1085
1644
|
}
|
|
1086
1645
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1646
|
+
const remainingConfigured = pairing.connections.filter((connection) => !skippedConnections.has(connection.connectionId));
|
|
1647
|
+
if (!options.shouldStop() && mayClaim() && repositoryPollableConnections.length > 0
|
|
1648
|
+
&& repositoryPollableConnections.length === pollableConnections.length
|
|
1649
|
+
&& pollableConnections.length === remainingConfigured.length
|
|
1650
|
+
&& Date.now() >= nextInvestigationPollAt) {
|
|
1651
|
+
try {
|
|
1652
|
+
const investigation = await client.claimNextInvestigation();
|
|
1653
|
+
({ consecutiveFailures: investigationPollFailures } = nextInvestigationPollBackoff(investigationPollFailures, true));
|
|
1654
|
+
nextInvestigationPollAt = 0;
|
|
1655
|
+
if (investigation) {
|
|
1656
|
+
claimedAny = true;
|
|
1657
|
+
recordCompletedClaim(await runOneInvestigation(options.anthropic, client, pollableConnections, runtimes, investigation, { log, error }, repositoryProvider));
|
|
1658
|
+
}
|
|
1095
1659
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1660
|
+
catch (reason) {
|
|
1661
|
+
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1662
|
+
return tokenRejected();
|
|
1663
|
+
const backoff = nextInvestigationPollBackoff(investigationPollFailures, false);
|
|
1664
|
+
investigationPollFailures = backoff.consecutiveFailures;
|
|
1665
|
+
nextInvestigationPollAt = Date.now() + backoff.delayMs;
|
|
1666
|
+
if (investigationPollFailures === 1) {
|
|
1667
|
+
error('Investigation poll failed:', reason instanceof Error ? reason.message : reason, `Retrying in ${backoff.delayMs / 1000}s.`);
|
|
1668
|
+
}
|
|
1669
|
+
else {
|
|
1670
|
+
warn(`Investigation poll still failing (${investigationPollFailures} consecutive); retrying in ${backoff.delayMs / 1000}s.`);
|
|
1671
|
+
}
|
|
1098
1672
|
}
|
|
1099
1673
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1674
|
+
if (claimedAny) {
|
|
1675
|
+
lastIdleStatusAt = Date.now();
|
|
1676
|
+
}
|
|
1677
|
+
else if (!options.shouldStop()) {
|
|
1678
|
+
const now = Date.now();
|
|
1679
|
+
if (now - lastIdleStatusAt >= IDLE_STATUS_MS) {
|
|
1680
|
+
log(`[${new Date(now).toISOString()}] still watching — no work claimed.`);
|
|
1681
|
+
lastIdleStatusAt = now;
|
|
1682
|
+
}
|
|
1683
|
+
await new Promise((done) => setTimeout(done, IDLE_POLL_MS));
|
|
1109
1684
|
}
|
|
1110
|
-
await new Promise((done) => setTimeout(done, IDLE_POLL_MS));
|
|
1111
1685
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1686
|
+
catch (reason) {
|
|
1687
|
+
if (reason instanceof CompanionHttpError && reason.status === 401)
|
|
1688
|
+
return tokenRejected();
|
|
1689
|
+
error('Unexpected run-loop error:', reason instanceof Error ? reason.message : reason);
|
|
1690
|
+
if (!options.shouldStop())
|
|
1691
|
+
await new Promise((done) => setTimeout(done, LOOP_RECOVERY_DELAY_MS));
|
|
1692
|
+
}
|
|
1119
1693
|
}
|
|
1120
1694
|
}
|
|
1695
|
+
finally {
|
|
1696
|
+
await wholeReadPool.shutdown();
|
|
1697
|
+
await sharedSources.shutdown();
|
|
1698
|
+
}
|
|
1121
1699
|
log('Stopped.');
|
|
1122
1700
|
return 'stopped';
|
|
1123
1701
|
}
|
|
1124
|
-
async function
|
|
1702
|
+
async function runCompanionLoop(options = {}) {
|
|
1125
1703
|
let config;
|
|
1126
1704
|
try {
|
|
1127
1705
|
config = loadConfig();
|
|
@@ -1271,6 +1849,22 @@ async function cmdRun(options = {}) {
|
|
|
1271
1849
|
}
|
|
1272
1850
|
}));
|
|
1273
1851
|
}
|
|
1852
|
+
async function cmdRun(options = {}) {
|
|
1853
|
+
// Cleanup is deliberately not flag-gated: source left by an older enabled build must still be
|
|
1854
|
+
// removed after rollback or while its Console is offline. Importing the module starts no timer.
|
|
1855
|
+
try {
|
|
1856
|
+
await startAttemptSourceSweeper();
|
|
1857
|
+
}
|
|
1858
|
+
catch {
|
|
1859
|
+
console.error('[whole-read-temp] cleanup_sweep_failed');
|
|
1860
|
+
}
|
|
1861
|
+
try {
|
|
1862
|
+
await runCompanionLoop(options);
|
|
1863
|
+
}
|
|
1864
|
+
finally {
|
|
1865
|
+
stopAttemptSourceSweeper();
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1274
1868
|
async function cmdRunWithInstance(options = {}) {
|
|
1275
1869
|
if (process.env.PRODUCT_CONSOLE_COMPANION_DAEMON_CHILD === '1')
|
|
1276
1870
|
return cmdRun(options);
|
|
@@ -1393,9 +1987,8 @@ async function cmdDisconnect(argv) {
|
|
|
1393
1987
|
await removeManagedCopies(removed.flatMap((pairing) => pairing.connections));
|
|
1394
1988
|
logRestartHintIfRunning();
|
|
1395
1989
|
}
|
|
1396
|
-
/**
|
|
1397
|
-
*
|
|
1398
|
-
* <config dir>/repos/ are ever deleted, and only for connections marked managed. */
|
|
1990
|
+
/** Legacy disconnect behavior. B38-c owns deletion for migrated storage; until then those paths
|
|
1991
|
+
* are left untouched. A legacy copy another remaining connection uses is also kept. */
|
|
1399
1992
|
async function removeManagedCopies(removedConnections) {
|
|
1400
1993
|
const reposDir = managedReposDir();
|
|
1401
1994
|
const stillUsed = new Set((loadConfig()?.pairings ?? [])
|
|
@@ -1403,7 +1996,7 @@ async function removeManagedCopies(removedConnections) {
|
|
|
1403
1996
|
.map((connection) => resolve(connection.repoPath).toLowerCase()));
|
|
1404
1997
|
const handled = new Set();
|
|
1405
1998
|
for (const connection of removedConnections) {
|
|
1406
|
-
if (!connection.managed)
|
|
1999
|
+
if (!connection.managed || usesTemporaryManagedStorage(connection))
|
|
1407
2000
|
continue;
|
|
1408
2001
|
const key = resolve(connection.repoPath).toLowerCase();
|
|
1409
2002
|
if (stillUsed.has(key) || handled.has(key))
|
|
@@ -1427,7 +2020,7 @@ async function cmdSignin(argv) {
|
|
|
1427
2020
|
let wanted = null;
|
|
1428
2021
|
if (flags.repo !== undefined) {
|
|
1429
2022
|
try {
|
|
1430
|
-
wanted =
|
|
2023
|
+
wanted = githubLabel(parseGitHubRepo(flags.repo)).toLowerCase();
|
|
1431
2024
|
}
|
|
1432
2025
|
catch (error) {
|
|
1433
2026
|
console.error(error instanceof ManagedRepoError ? error.message : String(error));
|
|
@@ -1435,24 +2028,39 @@ async function cmdSignin(argv) {
|
|
|
1435
2028
|
return;
|
|
1436
2029
|
}
|
|
1437
2030
|
}
|
|
1438
|
-
const
|
|
2031
|
+
const targets = new Map();
|
|
1439
2032
|
for (const pairing of loadConfig()?.pairings ?? []) {
|
|
1440
2033
|
const client = new CompanionClient(pairing.url, pairing.token);
|
|
1441
2034
|
for (const connection of pairing.connections) {
|
|
1442
2035
|
if (!connection.managed)
|
|
1443
2036
|
continue;
|
|
1444
|
-
|
|
1445
|
-
|
|
2037
|
+
let repository;
|
|
2038
|
+
try {
|
|
2039
|
+
repository = configuredManagedRepository(connection);
|
|
2040
|
+
}
|
|
2041
|
+
catch {
|
|
2042
|
+
continue;
|
|
2043
|
+
}
|
|
2044
|
+
const label = githubLabel(repository);
|
|
2045
|
+
const key = `${normalizeConsoleUrl(pairing.url)}\0${pairing.workspaceId ?? ''}\0${label.toLowerCase()}`;
|
|
2046
|
+
if (targets.has(key) || (wanted !== null && label.toLowerCase() !== wanted))
|
|
1446
2047
|
continue;
|
|
1447
|
-
|
|
1448
|
-
|
|
2048
|
+
targets.set(key, usesTemporaryManagedStorage(connection) ? {
|
|
2049
|
+
storage: 'temporary',
|
|
2050
|
+
repository,
|
|
2051
|
+
label,
|
|
2052
|
+
branch: connection.branch,
|
|
2053
|
+
getToken: () => client.githubToken(label).then((response) => response.token),
|
|
2054
|
+
} : {
|
|
2055
|
+
storage: 'persistent',
|
|
2056
|
+
repository,
|
|
1449
2057
|
repoPath: connection.repoPath,
|
|
1450
2058
|
label,
|
|
1451
2059
|
getToken: () => client.githubToken(label).then((response) => response.token),
|
|
1452
2060
|
});
|
|
1453
2061
|
}
|
|
1454
2062
|
}
|
|
1455
|
-
if (
|
|
2063
|
+
if (targets.size === 0) {
|
|
1456
2064
|
console.log(wanted === null
|
|
1457
2065
|
? "There's no companion-kept copy of any repo on this computer."
|
|
1458
2066
|
: `There's no companion-kept copy of ${flags.repo} on this computer.`);
|
|
@@ -1460,7 +2068,24 @@ async function cmdSignin(argv) {
|
|
|
1460
2068
|
process.exitCode = 1;
|
|
1461
2069
|
return;
|
|
1462
2070
|
}
|
|
1463
|
-
for (const
|
|
2071
|
+
for (const target of targets.values()) {
|
|
2072
|
+
const { label, getToken } = target;
|
|
2073
|
+
if (target.storage === 'temporary') {
|
|
2074
|
+
try {
|
|
2075
|
+
await proveManagedRepositoryAccess({
|
|
2076
|
+
repository: target.repository,
|
|
2077
|
+
branch: target.branch,
|
|
2078
|
+
credential: await getToken(),
|
|
2079
|
+
}, { managedRepositoryUrl: (repository) => testCloneUrl(repository) ?? githubCloneUrl(repository) });
|
|
2080
|
+
console.log(`GitHub access is ready for ${label}.`);
|
|
2081
|
+
}
|
|
2082
|
+
catch {
|
|
2083
|
+
console.log(`Still can't reach ${label} on GitHub. Reconnect GitHub in the Console.`);
|
|
2084
|
+
process.exitCode = 1;
|
|
2085
|
+
}
|
|
2086
|
+
continue;
|
|
2087
|
+
}
|
|
2088
|
+
const { repoPath } = target;
|
|
1464
2089
|
if (!existsSync(repoPath)) {
|
|
1465
2090
|
console.log(`The companion's copy of ${label} is missing. Run the connect command for it again.`);
|
|
1466
2091
|
process.exitCode = 1;
|
|
@@ -1496,6 +2121,11 @@ function cmdStatus() {
|
|
|
1496
2121
|
const workspace = pairing.workspaceId ? ` (workspace ${pairing.workspaceId.slice(0, 8)}…)` : '';
|
|
1497
2122
|
console.log(` ${pairing.url}${workspace}`);
|
|
1498
2123
|
for (const project of pairing.projects) {
|
|
2124
|
+
if (project.managed && project.repositoryStorage === 'temporary') {
|
|
2125
|
+
const label = project.repository?.path ?? project.label ?? labelFromCopyPath(project.repoPath);
|
|
2126
|
+
console.log(` ${project.connectionId} — ${label} — temporary repository access`);
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
1499
2129
|
console.log(` ${project.connectionId} — ${project.repoPath}`);
|
|
1500
2130
|
if (!project.managed)
|
|
1501
2131
|
continue;
|