Review found the owning documentation still describing the behaviour this branch replaced, in one case stacked directly above the new docstring saying the opposite. Holding a session proves containment, not ownership, so every text that called store membership the authoritative mapping was actively misleading for the module whose wrong answer misroutes every send. Corrected in the module docstring, the resolution module's precedence description, the sync-refs helper it points at, and the sync DOCUMENTATION.md table and rules. The debug report built its authoritative value membership-first, so for exactly the scenario this branch fixes it reported the parent directory and could raise a source-disagreement alert while routing was in fact correct. It now uses the same record-first order as the resolver. The CLI timeout comment claimed the wait and provisioning windows were additive while the code took the larger of the two. The server provisions the worktree inside session creation, before it waits for the session to go idle, so they do run in sequence: the windows are now summed and the tests pin both cases.
57 lines
2.9 KiB
JavaScript
57 lines
2.9 KiB
JavaScript
import { TunnelCliError, EXIT_CODE } from './cli-errors.js';
|
|
import { requestJson } from './cli-http.js';
|
|
|
|
const asNonEmptyString = (value) => {
|
|
if (typeof value !== 'string') return null;
|
|
const trimmed = value.trim();
|
|
return trimmed.length > 0 ? trimmed : null;
|
|
};
|
|
|
|
const DEFAULT_WAIT_TIMEOUT_SECONDS = 600;
|
|
const WAIT_HTTP_TIMEOUT_BUFFER_MS = 30_000;
|
|
|
|
// Provisioning a worktree is not one of the instant control calls the short
|
|
// default is sized for: it runs git against the repository and prepares a new
|
|
// directory, which on a cold path takes longer than the default allows. The
|
|
// server finishes the work regardless of the client giving up, so a client-side
|
|
// timeout here reported a failure for a worktree that was in fact created.
|
|
const WORKTREE_PROVISION_TIMEOUT_MS = 120_000;
|
|
|
|
// The control service blocks server-side while wait is set, so the client
|
|
// HTTP timeout must outlive the requested wait window instead of the short
|
|
// default used for instant control calls.
|
|
export const resolveControlTimeoutMs = (input, options) => {
|
|
if (Number.isFinite(options?.timeoutMs) && options.timeoutMs > 0) return options.timeoutMs;
|
|
const provisionsWorktree = asNonEmptyString(input?.worktree) !== null;
|
|
if (input?.wait !== true) {
|
|
return provisionsWorktree ? WORKTREE_PROVISION_TIMEOUT_MS : undefined;
|
|
}
|
|
const waitSeconds = Number(input?.timeout) > 0 ? Number(input.timeout) : DEFAULT_WAIT_TIMEOUT_SECONDS;
|
|
const waitTimeoutMs = (waitSeconds * 1000) + WAIT_HTTP_TIMEOUT_BUFFER_MS;
|
|
// The server provisions the worktree inside session creation, before it
|
|
// starts waiting for the session to go idle, so the two windows run in
|
|
// sequence rather than overlapping. The client window has to cover both.
|
|
return provisionsWorktree ? waitTimeoutMs + WORKTREE_PROVISION_TIMEOUT_MS : waitTimeoutMs;
|
|
};
|
|
|
|
export const requestControlAction = async (port, action, input, options = {}) => {
|
|
const timeoutMs = resolveControlTimeoutMs(input, options);
|
|
const { response, body } = await requestJson(port, '/api/openchamber/control', {
|
|
...options,
|
|
...(timeoutMs ? { timeoutMs } : {}),
|
|
method: 'POST',
|
|
body: JSON.stringify({ action, input }),
|
|
});
|
|
if (response?.ok) return body;
|
|
const isPartial = body?.partial === true;
|
|
const partialSessionId = isPartial ? asNonEmptyString(body?.sessionId) : null;
|
|
const partialDirectory = isPartial ? asNonEmptyString(body?.directory) : null;
|
|
const partialSubject = body?.partialAction === 'goal-configured' ? 'Goal on session' : 'Forked session';
|
|
const partial = partialSessionId
|
|
? ` ${partialSubject} ${partialSessionId} remains available${partialDirectory ? ` in ${partialDirectory}` : ''}.`
|
|
: '';
|
|
const message = `${asNonEmptyString(body?.error) || `Failed to execute ${action}`}${partial}`;
|
|
const status = Number(response?.status);
|
|
throw new TunnelCliError(message, status === 400 || status === 404 ? EXIT_CODE.USAGE_ERROR : EXIT_CODE.GENERAL_ERROR);
|
|
};
|