@galda/cli 0.10.121 → 0.10.122

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.
@@ -23,7 +23,7 @@
23
23
  // review_approved / review_dismissed → on the review action
24
24
 
25
25
  import { readAnalyticsPref, resolveAnalyticsEnabled } from './analytics-pref.mjs';
26
- import { readRelayPref, resolveRelayEnabled } from './relay-pref.mjs';
26
+ import { readRelayPref, resolveRemoteAccess, relayClientRunning } from './relay-pref.mjs';
27
27
 
28
28
  const BILLING_API_URL = (process.env.MANAGER_BILLING_API_URL ?? '').replace(/\/$/, '');
29
29
 
@@ -48,7 +48,9 @@ export function analyticsEnabled() {
48
48
  return resolveAnalyticsEnabled({
49
49
  configured: Boolean(BILLING_API_URL),
50
50
  pref: dataDir ? readAnalyticsPref(dataDir) : null,
51
- relayEnabled: dataDir ? resolveRelayEnabled({ relayUrl, pref: readRelayPref(dataDir) }) : undefined,
51
+ relayEnabled: dataDir
52
+ ? resolveRemoteAccess({ relayUrl, pref: readRelayPref(dataDir), relayRunning: relayClientRunning(dataDir) }).enabled
53
+ : undefined,
52
54
  });
53
55
  }
54
56
 
@@ -18,6 +18,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
19
 
20
20
  export const RELAY_PREF_FILE = 'relay-pref.json';
21
+ export const RELAY_LOCK_FILE = 'relay-client.lock';
21
22
 
22
23
  export function relayPrefPath(dataDir) {
23
24
  return join(dataDir, RELAY_PREF_FILE);
@@ -55,3 +56,43 @@ export function writeRelayPref(dataDir, enabled) {
55
56
  writeFileSync(relayPrefPath(dataDir), JSON.stringify({ enabled: value, at: Date.now() }) + '\n');
56
57
  return value;
57
58
  }
59
+
60
+ // Is remote access actually on, from the server's point of view?
61
+ //
62
+ // `resolveRelayEnabled` above answers the LAUNCHER's question — "should I start a
63
+ // relay?" — from the URL it was configured with. The server cannot use the same
64
+ // answer, because it does not always have that env var: Masa's resident instance
65
+ // is launched by launchd as `node engine/server.mjs`, not through
66
+ // `bin/manager-for-ai.mjs` which is where RELAY_URL gets set. On 2026-08-02 that
67
+ // made the server report `relayEnabled:false` while a relay-client was connected
68
+ // and serving app.galda.app — so Settings said "remote access is off" to someone
69
+ // whose remote access was on, and the analytics gate (analytics-pref.mjs) went
70
+ // quiet on that install for a reason that was not true.
71
+ //
72
+ // A running relay-client is the ground truth: it holds a PID lock in the same
73
+ // DATA_DIR, which is what bin/manager-for-ai.mjs already checks for its
74
+ // duplicate-instance guard. Env is the fallback, not the authority.
75
+ //
76
+ // pref === false → off. The person said so, and that beats any evidence.
77
+ // otherwise → on if a relay is running OR one is configured.
78
+ export function resolveRemoteAccess({ relayUrl, pref, relayRunning } = {}) {
79
+ const configured = Boolean(relayUrl) || Boolean(relayRunning);
80
+ return { configured, enabled: configured && pref !== false };
81
+ }
82
+
83
+ // Read the relay-client's PID lock and ask the OS whether that process is alive.
84
+ // `isAlive` is injected so the decision stays testable without spawning anything;
85
+ // callers pass a real probe (see relayClientRunning below).
86
+ export function relayLockIsLive(raw, isAlive) {
87
+ let lock;
88
+ try { lock = JSON.parse(String(raw ?? '')); } catch { return false; }
89
+ const pid = Number(lock?.pid);
90
+ if (!Number.isInteger(pid) || pid <= 0) return false;
91
+ try { return Boolean(isAlive(pid)); } catch { return false; }
92
+ }
93
+
94
+ export function relayClientRunning(dataDir) {
95
+ let raw = '';
96
+ try { raw = readFileSync(join(dataDir, RELAY_LOCK_FILE), 'utf8'); } catch { return false; }
97
+ return relayLockIsLive(raw, (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } });
98
+ }
package/engine/server.mjs CHANGED
@@ -24,7 +24,7 @@ import { bootBanner, bootDestination, folderLabel, chooseFolderScript, parseChos
24
24
  import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildReviewAttemptLedger, latestGoalOutcomeTask, buildDirtyWorkspacePrBlock, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, rebasePreviewUrl, shouldCreateGoalPR, normalizePullRequestUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, DEFAULT_PRIORITY, normalizePriority, isAcceptedPriority, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, resolveTestGate, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, buildQueueWaits, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, autoFoldReviewsEnabled, parseGitUnifiedDiffLocations, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyPrSafety, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, buildVerificationInfraError, verificationInfraErrorFrom, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, licenseTokenPayload, shouldRefreshLicense, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, testFailureReason, manualTestRetryPrompt, isNothingVerifiable, isNothingVerifiableForPR, isSuspiciouslyIncompleteDone, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, REQUIREMENT_EVIDENCE_FILE, parseRequirementEvidenceDocument, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession, parseApprovalRequest } from './lib.mjs';
25
25
  import { migrateRequirementModel, validateRequirementModel } from './requirement-model.mjs';
26
26
  import { buildManagerVerificationChecks, buildRequirementVerificationEvidence, mergeRequirementVerificationEvidence } from './requirement-verification.mjs';
27
- import { readRelayPref, writeRelayPref, resolveRelayEnabled } from './relay-pref.mjs';
27
+ import { readRelayPref, writeRelayPref, resolveRemoteAccess, relayClientRunning } from './relay-pref.mjs';
28
28
  import { readAnalyticsPref, writeAnalyticsPref, resolveAnalyticsEnabled } from './analytics-pref.mjs';
29
29
  import { createSerialQueue } from './lib.mjs';
30
30
  import { wantsPullRequest } from './lib.mjs';
@@ -4463,6 +4463,16 @@ const server = createServer(async (req, res) => {
4463
4463
  return;
4464
4464
  }
4465
4465
 
4466
+ // What the server can honestly say about remote access. NOT `RELAY_URL` alone:
4467
+ // launchd starts this process as `node engine/server.mjs`, so the variable
4468
+ // bin/manager-for-ai.mjs sets is simply absent there while a relay-client is
4469
+ // connected and serving app.galda.app. A live relay lock is the ground truth.
4470
+ const remoteAccess = () => resolveRemoteAccess({
4471
+ relayUrl: process.env.RELAY_URL,
4472
+ pref: readRelayPref(DATA_DIR),
4473
+ relayRunning: relayClientRunning(DATA_DIR),
4474
+ });
4475
+
4466
4476
  // GET /api/relay-status — is this account's relay slot held by ANOTHER device?
4467
4477
  // The relay-client (a separate process) writes relay-status.json when it stands
4468
4478
  // down after being evicted (see relay-client.mjs / lib.mjs relayReconnectDecision).
@@ -4490,11 +4500,8 @@ const server = createServer(async (req, res) => {
4490
4500
  // relay-takeover.req above.
4491
4501
  if (url.pathname === '/api/relay-pref' && req.method === 'GET') {
4492
4502
  const pref = readRelayPref(DATA_DIR);
4493
- return json(res, 200, {
4494
- pref,
4495
- enabled: resolveRelayEnabled({ relayUrl: process.env.RELAY_URL, pref }),
4496
- configured: Boolean(process.env.RELAY_URL),
4497
- });
4503
+ const ra = remoteAccess();
4504
+ return json(res, 200, { pref, enabled: ra.enabled, configured: ra.configured });
4498
4505
  }
4499
4506
  if (url.pathname === '/api/relay-pref' && req.method === 'POST') {
4500
4507
  let body = '';
@@ -4508,11 +4515,8 @@ const server = createServer(async (req, res) => {
4508
4515
  let saved;
4509
4516
  try { saved = writeRelayPref(DATA_DIR, input.enabled); }
4510
4517
  catch (e) { return json(res, 500, { error: String(e?.message ?? e) }); }
4511
- return json(res, 200, {
4512
- pref: saved,
4513
- enabled: resolveRelayEnabled({ relayUrl: process.env.RELAY_URL, pref: saved }),
4514
- configured: Boolean(process.env.RELAY_URL),
4515
- });
4518
+ const ra = resolveRemoteAccess({ relayUrl: process.env.RELAY_URL, pref: saved, relayRunning: relayClientRunning(DATA_DIR) });
4519
+ return json(res, 200, { pref: saved, enabled: ra.enabled, configured: ra.configured });
4516
4520
  });
4517
4521
  return;
4518
4522
  }
@@ -4524,7 +4528,7 @@ const server = createServer(async (req, res) => {
4524
4528
  // nothing.
4525
4529
  if (url.pathname === '/api/analytics-pref' && req.method === 'GET') {
4526
4530
  const pref = readAnalyticsPref(DATA_DIR);
4527
- const relayEnabled = resolveRelayEnabled({ relayUrl: process.env.RELAY_URL, pref: readRelayPref(DATA_DIR) });
4531
+ const relayEnabled = remoteAccess().enabled;
4528
4532
  return json(res, 200, {
4529
4533
  pref,
4530
4534
  enabled: resolveAnalyticsEnabled({ configured: Boolean(BILLING_API_URL), pref, relayEnabled }),
@@ -4545,7 +4549,7 @@ const server = createServer(async (req, res) => {
4545
4549
  let saved;
4546
4550
  try { saved = writeAnalyticsPref(DATA_DIR, input.enabled); }
4547
4551
  catch (e) { return json(res, 500, { error: String(e?.message ?? e) }); }
4548
- const relayEnabled = resolveRelayEnabled({ relayUrl: process.env.RELAY_URL, pref: readRelayPref(DATA_DIR) });
4552
+ const relayEnabled = remoteAccess().enabled;
4549
4553
  return json(res, 200, {
4550
4554
  pref: saved,
4551
4555
  enabled: resolveAnalyticsEnabled({ configured: Boolean(BILLING_API_URL), pref: saved, relayEnabled }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.121",
3
+ "version": "0.10.122",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to Claude Code or Codex, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {