4runr-os 1.3.21 → 1.3.23

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.
@@ -15,7 +15,7 @@
15
15
  * - Side-effect free
16
16
  * - Deterministic
17
17
  */
18
- import { available, unavailable, loading, isLoading } from '../state/value.js';
18
+ import { available, unavailable } from '../state/value.js';
19
19
  import { listAgents, getAgent, createAgent as v1CreateAgent } from '../v1Adapters/agents.js';
20
20
  import { connect as v1Connect, getConnectionState } from '../v1Adapters/connect.js';
21
21
  import { startRun, listRuns } from '../v1Adapters/runs.js';
@@ -241,7 +241,7 @@ function handleUnknown(cmd) {
241
241
  * Network connection attempt ID tracker
242
242
  * Prevents stale updates from overlapping connection attempts
243
243
  */
244
- let networkAttemptId = 0;
244
+ // networkAttemptId removed - using gatewayConnectionStore instead
245
245
  /**
246
246
  * Command registration map
247
247
  *
@@ -255,6 +255,7 @@ const commands = {
255
255
  // Section 6: Real commands (V1-backed)
256
256
  'agents': handleAgents,
257
257
  'connect': handleConnect,
258
+ 'verify': handleVerify,
258
259
  'start': handleStart,
259
260
  'runs': handleRuns,
260
261
  'system': handleSystem,
@@ -339,6 +340,13 @@ function handleHelp(ctx, args) {
339
340
  level: 'INFO',
340
341
  msg: ' connect — connect to Gateway',
341
342
  },
343
+ {
344
+ id: `help-verify-${Date.now()}`,
345
+ ts: Date.now(),
346
+ tag: 'SYS',
347
+ level: 'INFO',
348
+ msg: ' verify - Test connection with real API calls',
349
+ },
342
350
  {
343
351
  id: `help-10-${Date.now()}`,
344
352
  ts: Date.now(),
@@ -739,14 +747,12 @@ async function handleAgentsInspect(ctx, args) {
739
747
  * - Guarantees reset in finally block
740
748
  */
741
749
  async function handleConnect(ctx, args) {
742
- // Increment attempt ID for this connection attempt
743
- networkAttemptId++;
744
- const currentAttemptId = networkAttemptId;
745
- // Section 6.2: Ignore any arguments (parser may still accept them, but they are ignored)
746
- // Always use canonical Gateway URL
750
+ // Import store and gateway config
751
+ const { gatewayConnectionStore } = await import('../state/gatewayConnectionStore.js');
747
752
  const gatewayConfig = await import('../config/gateway.js');
748
- const resolvedTarget = gatewayConfig.resolveGatewayUrl();
749
- // Step 1: Emit [CMD] connect
753
+ // Step 1: Resolve target with source tracking
754
+ const { url: inputTarget, source } = gatewayConfig.resolveGatewayUrlWithSource();
755
+ // Step 2: Emit [CMD] connect
750
756
  const cmdEvent = {
751
757
  id: `cmd-${Date.now()}`,
752
758
  ts: Date.now(),
@@ -754,109 +760,34 @@ async function handleConnect(ctx, args) {
754
760
  level: 'INFO',
755
761
  msg: 'connect',
756
762
  };
757
- // Step 2: Parse URL and emit target info
758
- let parsedHost = 'unknown';
759
- let parsedPort = 'unknown';
760
- try {
761
- const url = new URL(resolvedTarget);
762
- parsedHost = url.hostname;
763
- parsedPort = url.port || (url.protocol === 'https:' ? '443' : '80');
764
- }
765
- catch {
766
- // URL parse failed - use as-is
767
- }
768
- const targetEvent = {
769
- id: `sys-target-${Date.now()}`,
763
+ const events = [cmdEvent];
764
+ // Step 3: Log target source
765
+ events.push({
766
+ id: `sys-source-${Date.now()}`,
770
767
  ts: Date.now(),
771
768
  tag: 'SYS',
772
769
  level: 'INFO',
773
- msg: `connect: target=${resolvedTarget} host=${parsedHost} port=${parsedPort}`,
774
- };
775
- // Step 3: Run connection probes (TCP + HTTP) - but only show if they succeed
776
- // If localhost fails, we'll auto-fallback and show probes for the successful endpoint
777
- const events = [cmdEvent, targetEvent];
778
- // Only run probes if we're not on Windows/Mac (where auto-fallback will handle it)
779
- // Or if we're on Windows/Mac but not using localhost
780
- const isLocalhost = resolvedTarget.includes('127.0.0.1') || resolvedTarget.includes('localhost');
781
- const isWindowsOrMac = process.platform === 'win32' || process.platform === 'darwin';
782
- const shouldSkipProbes = isLocalhost && isWindowsOrMac; // Skip localhost probes on Windows/Mac (auto-fallback will handle)
783
- if (!shouldSkipProbes) {
784
- try {
785
- const { runConnectionProbes } = await import('../v1Adapters/connect.js');
786
- const probeResults = await runConnectionProbes(resolvedTarget);
787
- // Only show successful probes (cleaner UI)
788
- for (const probe of probeResults) {
789
- if (probe.success) {
790
- if (probe.type === 'tcp') {
791
- events.push({
792
- id: `sys-tcp-${Date.now()}`,
793
- ts: Date.now(),
794
- tag: 'SYS',
795
- level: 'INFO',
796
- msg: `tcp: ${probe.details}`,
797
- });
798
- }
799
- else if (probe.type === 'http') {
800
- events.push({
801
- id: `sys-http-${Date.now()}`,
802
- ts: Date.now(),
803
- tag: 'SYS',
804
- level: 'INFO',
805
- msg: `http: ${probe.details || 'ok'}`,
806
- });
807
- }
808
- }
809
- }
810
- }
811
- catch (probeError) {
812
- // Probe errors are silent - connection attempt will show the real result
813
- }
814
- }
815
- // Step 4: Emit [SYS] CONNECTING...
816
- const connectingEvent = {
770
+ msg: `gateway target source: ${source}`,
771
+ });
772
+ // Step 4: Set connecting state in store
773
+ gatewayConnectionStore.setConnecting(inputTarget, 5000);
774
+ // Step 5: Emit connecting event
775
+ events.push({
817
776
  id: `sys-connecting-${Date.now()}`,
818
777
  ts: Date.now(),
819
778
  tag: 'SYS',
820
779
  level: 'INFO',
821
780
  msg: 'CONNECTING...',
822
- };
823
- events.push(connectingEvent);
824
- // Step 4: Update UiState.network → LOADING (with attemptId in meta)
825
- const loadingStateUpdate = (prev) => ({
826
- ...prev,
827
- network: loading('Connecting to Gateway...', {
828
- attemptId: currentAttemptId,
829
- target: resolvedTarget,
830
- timeout: 5,
831
- }),
832
- statusStrip: available({
833
- left: 'NET: CONNECTING...',
834
- right: 'GATEWAY: CONNECTING',
835
- }),
836
781
  });
837
- // Step 5: Await connectAdapter with timeout (always uses canonical URL)
782
+ // Step 6: Attempt connection
838
783
  try {
839
784
  const result = await v1Connect();
840
- // Step 6: Check if this attempt is still current (prevent stale updates)
841
- if (currentAttemptId !== networkAttemptId) {
842
- // Stale completion - ignore it
843
- return {
844
- events: [cmdEvent], // Only emit [CMD], no state update
845
- };
846
- }
847
785
  if (!result.ok) {
848
- // Failure: network UNAVAILABLE
849
- // Section 6.2: Map to clear error message
786
+ // Failure: Update store with error
850
787
  const errorCode = result.error.code || 'UNKNOWN';
851
- let errorMsg = 'Gateway unreachable';
852
- // Map specific error codes to clear messages
853
- if (errorCode === 'ECONNREFUSED' || errorCode === 'ETIMEDOUT' || errorCode === 'ECONNABORTED' ||
854
- errorCode === 'GATEWAY_TIMEOUT' || errorCode === 'GATEWAY_UNREACHABLE') {
855
- errorMsg = 'Gateway unreachable (ECONNREFUSED | ETIMEDOUT | ECONNABORTED)';
856
- }
857
- else {
858
- errorMsg = result.error.message || 'Gateway unreachable';
859
- }
788
+ const errorMsg = result.error.message || 'Gateway unreachable';
789
+ const nextAction = result.error.nextAction || 'Retry "connect"';
790
+ gatewayConnectionStore.setError(inputTarget, errorCode, errorMsg, nextAction);
860
791
  events.push({
861
792
  id: `err-${Date.now()}`,
862
793
  ts: Date.now(),
@@ -864,34 +795,43 @@ async function handleConnect(ctx, args) {
864
795
  level: 'ERROR',
865
796
  msg: errorMsg,
866
797
  });
867
- return {
868
- events,
869
- uiStateUpdate: (prev) => {
870
- // Section 6.2: Always update to UNAVAILABLE on failure
871
- // Check attemptId to prevent stale updates, but don't require LOADING state
872
- // (LOADING state might not have been applied if connection failed very quickly)
873
- const prevAttemptId = isLoading(prev.network) ? prev.network.meta?.attemptId : undefined;
874
- if (prevAttemptId === currentAttemptId || prevAttemptId === undefined) {
875
- return {
876
- ...prev,
877
- network: unavailable('Gateway unreachable', 'Start Gateway service and run "connect"'),
878
- statusStrip: available({
879
- left: 'OFFLINE',
880
- right: 'GATEWAY: UNAVAILABLE',
881
- }),
882
- };
883
- }
884
- return prev; // Stale attempt, don't update
885
- },
886
- };
798
+ // Emit store state for debugging
799
+ const storeState = gatewayConnectionStore.getState();
800
+ events.push({
801
+ id: `sys-network-state-${Date.now()}`,
802
+ ts: Date.now(),
803
+ tag: 'SYS',
804
+ level: 'INFO',
805
+ msg: `network_state=${storeState.status}`,
806
+ });
807
+ return { events };
887
808
  }
888
- // Success: network AVAILABLE
889
- // Verify connection by showing gateway info
890
- const connectedEndpoint = result.data.endpoint;
809
+ // Success: Update store with connection info
810
+ const effectiveTarget = result.data.endpoint; // The endpoint that actually worked (might be different from input)
891
811
  const healthData = result.data.healthData;
892
812
  const latency = result.data.latency;
813
+ gatewayConnectionStore.setConnected(inputTarget, effectiveTarget, healthData, latency);
814
+ // Show input vs effective target if different
815
+ if (effectiveTarget !== inputTarget) {
816
+ events.push({
817
+ id: `sys-targets-${Date.now()}`,
818
+ ts: Date.now(),
819
+ tag: 'SYS',
820
+ level: 'INFO',
821
+ msg: `connect: input=${inputTarget} effective=${effectiveTarget}`,
822
+ });
823
+ }
824
+ else {
825
+ events.push({
826
+ id: `sys-target-${Date.now()}`,
827
+ ts: Date.now(),
828
+ tag: 'SYS',
829
+ level: 'INFO',
830
+ msg: `connect: target=${inputTarget}`,
831
+ });
832
+ }
893
833
  // Show clean connection message
894
- const endpointUrl = new URL(connectedEndpoint);
834
+ const endpointUrl = new URL(effectiveTarget);
895
835
  const displayHost = endpointUrl.hostname === '127.0.0.1' ? 'localhost' : endpointUrl.hostname;
896
836
  const displayPort = endpointUrl.port || (endpointUrl.protocol === 'https:' ? '443' : '80');
897
837
  events.push({
@@ -913,96 +853,213 @@ async function handleConnect(ctx, args) {
913
853
  msg: `Gateway: ${status} | Persistence: ${persistence}${latency ? ` | Latency: ${latency}ms` : ''}`,
914
854
  });
915
855
  }
916
- return {
917
- events,
918
- uiStateUpdate: (prev) => {
919
- // Section 6.2: Always update to AVAILABLE on success
920
- // Check attemptId to prevent stale updates, but don't require LOADING state
921
- const prevAttemptId = isLoading(prev.network) ? prev.network.meta?.attemptId : undefined;
922
- if (prevAttemptId === currentAttemptId || prevAttemptId === undefined) {
923
- // Use the actual endpoint that connected (might be different due to auto-fallback)
924
- const actualEndpoint = result.data.endpoint;
925
- const healthData = result.data.healthData;
926
- const latency = result.data.latency;
927
- return {
928
- ...prev,
929
- network: available({
930
- gateway: 'CONNECTED',
931
- endpoint: actualEndpoint,
932
- status: healthData?.status || 'alive',
933
- latency: latency,
934
- }),
935
- capabilities: available({
936
- items: [], // Empty is allowed for now
937
- }),
938
- statusStrip: available({
939
- left: 'ONLINE',
940
- right: 'GATEWAY: CONNECTED',
941
- }),
942
- };
943
- }
944
- return prev; // Stale attempt, don't update
945
- },
946
- };
856
+ // Emit store state for debugging
857
+ const storeState = gatewayConnectionStore.getState();
858
+ events.push({
859
+ id: `sys-network-state-${Date.now()}`,
860
+ ts: Date.now(),
861
+ tag: 'SYS',
862
+ level: 'INFO',
863
+ msg: `network_state=${storeState.status}`,
864
+ });
865
+ return { events };
947
866
  }
948
867
  catch (error) {
949
868
  // Step 7: Catch-all error handler
950
- // Check if this attempt is still current
951
- if (currentAttemptId !== networkAttemptId) {
952
- // Stale error - ignore it
953
- return {
954
- events: [cmdEvent],
955
- };
956
- }
957
- // Section 6.2: Map errors to clear error codes
869
+ const errorMsg = error?.message || 'Connection failed';
958
870
  const errorCode = error?.code || 'UNKNOWN';
959
- let errorMessage = 'Gateway unreachable';
960
- if (errorCode === 'ECONNREFUSED' || errorCode === 'ETIMEDOUT' || errorCode === 'ECONNABORTED') {
961
- errorMessage = 'Gateway unreachable (ECONNREFUSED | ETIMEDOUT | ECONNABORTED)';
962
- }
963
- else if (error?.message) {
964
- errorMessage = String(error.message).substring(0, 50).trim();
871
+ gatewayConnectionStore.setError(inputTarget, errorCode, errorMsg, 'Retry "connect"');
872
+ events.push({
873
+ id: `err-${Date.now()}`,
874
+ ts: Date.now(),
875
+ tag: 'ERR',
876
+ level: 'ERROR',
877
+ msg: errorMsg,
878
+ });
879
+ // Emit store state for debugging
880
+ const storeState = gatewayConnectionStore.getState();
881
+ events.push({
882
+ id: `sys-network-state-${Date.now()}`,
883
+ ts: Date.now(),
884
+ tag: 'SYS',
885
+ level: 'INFO',
886
+ msg: `network_state=${storeState.status}`,
887
+ });
888
+ return { events };
889
+ }
890
+ }
891
+ /**
892
+ * 4.5️⃣ verify
893
+ *
894
+ * Makes real API calls to prove the connection works
895
+ * Tests: /health, /ready, /api/runs, /metrics
896
+ */
897
+ async function handleVerify(ctx, args) {
898
+ const { gatewayConnectionStore } = await import('../state/gatewayConnectionStore.js');
899
+ const gatewayConfig = await import('../config/gateway.js');
900
+ const { GatewayClient } = await import('../../../gateway-client.js');
901
+ const cmdEvent = {
902
+ id: `cmd-${Date.now()}`,
903
+ ts: Date.now(),
904
+ tag: 'CMD',
905
+ level: 'INFO',
906
+ msg: 'verify',
907
+ };
908
+ const events = [cmdEvent];
909
+ // Check store state first
910
+ const storeState = gatewayConnectionStore.getState();
911
+ if (storeState.status !== 'connected') {
912
+ events.push({
913
+ id: `err-${Date.now()}`,
914
+ ts: Date.now(),
915
+ tag: 'ERR',
916
+ level: 'ERROR',
917
+ msg: `Not connected (status: ${storeState.status}). Run "connect" first.`,
918
+ });
919
+ return { events };
920
+ }
921
+ const gatewayUrl = gatewayConfig.resolveGatewayUrl();
922
+ const client = new GatewayClient({ gatewayUrl });
923
+ events.push({
924
+ id: `sys-verify-start-${Date.now()}`,
925
+ ts: Date.now(),
926
+ tag: 'SYS',
927
+ level: 'INFO',
928
+ msg: `Verifying connection to ${storeState.resolvedTo}...`,
929
+ });
930
+ // Test 1: Health check
931
+ try {
932
+ const startTime = Date.now();
933
+ const healthResponse = await client.health();
934
+ const latency = Date.now() - startTime;
935
+ events.push({
936
+ id: `ok-health-${Date.now()}`,
937
+ ts: Date.now(),
938
+ tag: 'OK',
939
+ level: 'INFO',
940
+ msg: `✓ /health: ${healthResponse.status || 'ok'} (${latency}ms)`,
941
+ });
942
+ if (healthResponse.persistence) {
943
+ events.push({
944
+ id: `sys-persistence-${Date.now()}`,
945
+ ts: Date.now(),
946
+ tag: 'SYS',
947
+ level: 'INFO',
948
+ msg: ` Persistence: ${healthResponse.persistence}`,
949
+ });
965
950
  }
966
- else if (typeof error === 'string') {
967
- errorMessage = error.substring(0, 50).trim();
951
+ }
952
+ catch (error) {
953
+ events.push({
954
+ id: `err-health-${Date.now()}`,
955
+ ts: Date.now(),
956
+ tag: 'ERR',
957
+ level: 'ERROR',
958
+ msg: `✗ /health failed: ${error.message || 'Unknown error'}`,
959
+ });
960
+ }
961
+ // Test 2: Ready check
962
+ try {
963
+ const startTime = Date.now();
964
+ const readyResponse = await fetch(`${gatewayUrl}/ready`, {
965
+ method: 'GET',
966
+ signal: AbortSignal.timeout(5000),
967
+ });
968
+ const latency = Date.now() - startTime;
969
+ const data = await readyResponse.json();
970
+ events.push({
971
+ id: `ok-ready-${Date.now()}`,
972
+ ts: Date.now(),
973
+ tag: 'OK',
974
+ level: 'INFO',
975
+ msg: `✓ /ready: ${data.status || readyResponse.status} (${latency}ms)`,
976
+ });
977
+ }
978
+ catch (error) {
979
+ events.push({
980
+ id: `err-ready-${Date.now()}`,
981
+ ts: Date.now(),
982
+ tag: 'ERR',
983
+ level: 'ERROR',
984
+ msg: `✗ /ready failed: ${error.message || 'Unknown error'}`,
985
+ });
986
+ }
987
+ // Test 3: List runs (requires auth, but tests API endpoint)
988
+ try {
989
+ const startTime = Date.now();
990
+ const runsResponse = await fetch(`${gatewayUrl}/api/runs?limit=1`, {
991
+ method: 'GET',
992
+ headers: {
993
+ 'Authorization': `Bearer ${process.env.GATEWAY_TOKEN || ''}`,
994
+ },
995
+ signal: AbortSignal.timeout(5000),
996
+ });
997
+ const latency = Date.now() - startTime;
998
+ if (runsResponse.ok) {
999
+ const data = await runsResponse.json();
1000
+ events.push({
1001
+ id: `ok-runs-${Date.now()}`,
1002
+ ts: Date.now(),
1003
+ tag: 'OK',
1004
+ level: 'INFO',
1005
+ msg: `✓ /api/runs: ${runsResponse.status} (${latency}ms) - ${data.runs?.length || 0} runs`,
1006
+ });
968
1007
  }
969
- if (!errorMessage || errorMessage === 'Error' || errorMessage.trim() === '') {
970
- errorMessage = 'Gateway unreachable';
1008
+ else {
1009
+ events.push({
1010
+ id: `warn-runs-${Date.now()}`,
1011
+ ts: Date.now(),
1012
+ tag: 'SYS',
1013
+ level: 'WARN',
1014
+ msg: `⚠ /api/runs: ${runsResponse.status} (${latency}ms) - ${runsResponse.status === 401 ? 'Auth required' : 'Error'}`,
1015
+ });
971
1016
  }
1017
+ }
1018
+ catch (error) {
972
1019
  events.push({
973
- id: `err-${Date.now()}`,
1020
+ id: `err-runs-${Date.now()}`,
974
1021
  ts: Date.now(),
975
1022
  tag: 'ERR',
976
1023
  level: 'ERROR',
977
- msg: errorMessage,
1024
+ msg: `✗ /api/runs failed: ${error.message || 'Unknown error'}`,
1025
+ });
1026
+ }
1027
+ // Test 4: Metrics endpoint
1028
+ try {
1029
+ const startTime = Date.now();
1030
+ const metricsResponse = await fetch(`${gatewayUrl}/metrics`, {
1031
+ method: 'GET',
1032
+ signal: AbortSignal.timeout(5000),
1033
+ });
1034
+ const latency = Date.now() - startTime;
1035
+ const text = await metricsResponse.text();
1036
+ const lines = text.split('\n').filter(l => l.trim() && !l.startsWith('#')).length;
1037
+ events.push({
1038
+ id: `ok-metrics-${Date.now()}`,
1039
+ ts: Date.now(),
1040
+ tag: 'OK',
1041
+ level: 'INFO',
1042
+ msg: `✓ /metrics: ${metricsResponse.status} (${latency}ms) - ${lines} metrics`,
978
1043
  });
979
- return {
980
- events,
981
- uiStateUpdate: (prev) => {
982
- // Section 6.2: Always update to UNAVAILABLE on error
983
- // Check attemptId to prevent stale updates, but don't require LOADING state
984
- const prevAttemptId = isLoading(prev.network) ? prev.network.meta?.attemptId : undefined;
985
- if (prevAttemptId === currentAttemptId || prevAttemptId === undefined) {
986
- return {
987
- ...prev,
988
- network: unavailable('Gateway unreachable', 'Start Gateway service and run "connect"'),
989
- statusStrip: available({
990
- left: 'OFFLINE',
991
- right: 'GATEWAY: UNAVAILABLE',
992
- }),
993
- };
994
- }
995
- return prev; // Stale attempt, don't update
996
- },
997
- };
998
1044
  }
999
- finally {
1000
- // Step 8: Finally block - guarantee network is not LOADING for this attemptId
1001
- // This is a safety net - if catch already set UNAVAILABLE, this does nothing
1002
- // But if something went wrong, we ensure LOADING doesn't persist
1003
- // Note: We can't update state here directly, but we've already handled it above
1004
- // This is just documentation that LOADING must not survive past command completion
1045
+ catch (error) {
1046
+ events.push({
1047
+ id: `err-metrics-${Date.now()}`,
1048
+ ts: Date.now(),
1049
+ tag: 'ERR',
1050
+ level: 'ERROR',
1051
+ msg: `✗ /metrics failed: ${error.message || 'Unknown error'}`,
1052
+ });
1005
1053
  }
1054
+ // Summary
1055
+ events.push({
1056
+ id: `sys-verify-done-${Date.now()}`,
1057
+ ts: Date.now(),
1058
+ tag: 'SYS',
1059
+ level: 'INFO',
1060
+ msg: 'Verification complete. Connection is working.',
1061
+ });
1062
+ return { events };
1006
1063
  }
1007
1064
  /**
1008
1065
  * 5️⃣ start <agentId>