@magnusekdahl/parallix 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,6 +51,7 @@ exports.isInvalidAgentConfigError = isInvalidAgentConfigError;
51
51
  exports.updateAgentBlock = updateAgentBlock;
52
52
  exports.resolveBlocklistTargetPath = resolveBlocklistTargetPath;
53
53
  exports.resolveNoOutputWatchdogConfig = resolveNoOutputWatchdogConfig;
54
+ exports.shouldPersistLaunchFailureBlock = shouldPersistLaunchFailureBlock;
54
55
  const node_fs_1 = __importDefault(require("node:fs"));
55
56
  const node_path_1 = __importDefault(require("node:path"));
56
57
  const node_child_process_1 = require("node:child_process");
@@ -107,6 +108,17 @@ const KNOWN_AGENT_NAMES = Object.freeze([
107
108
  'human'
108
109
  ]);
109
110
  exports.KNOWN_AGENT_NAMES = KNOWN_AGENT_NAMES;
111
+ const NON_BLOCKING_LAUNCH_ERROR_PATTERNS = Object.freeze([
112
+ /\b(?:invalid|unknown|unsupported|unrecognized)\s+model\b/i,
113
+ /\bmodel\s+(?:identifier|id)\s+(?:is\s+)?invalid\b/i,
114
+ /\b(?:model\s+not\s+found|no\s+such\s+model)\b/i,
115
+ /\bunknown\s+option\b/i,
116
+ /\bauth(?:entication)?\b/i,
117
+ /\bunauthorized\b/i,
118
+ /\bforbidden\b/i,
119
+ /\bapi\s+key\b/i,
120
+ /\bread-only file system\b/i
121
+ ]);
110
122
  function workflowLauncherStatus(agent) {
111
123
  const resolver = RESOLVERS[agent];
112
124
  if (!resolver) {
@@ -148,6 +160,21 @@ function commandInPath(name) {
148
160
  });
149
161
  return result.status === 0 && result.stdout.trim().length > 0;
150
162
  }
163
+ function shouldPersistLaunchFailureBlock(agent, result) {
164
+ if (!result || agent === 'custom') {
165
+ return false;
166
+ }
167
+ const combined = [
168
+ result.stderr || '',
169
+ result.stdout || '',
170
+ result.error?.message || '',
171
+ result.error?.code || ''
172
+ ].join('\n');
173
+ if (!combined.trim()) {
174
+ return true;
175
+ }
176
+ return !NON_BLOCKING_LAUNCH_ERROR_PATTERNS.some(pattern => pattern.test(combined));
177
+ }
151
178
  function buildInvalidAgentConfigError(configPath, scope, originalError) {
152
179
  const location = node_path_1.default.resolve(configPath);
153
180
  const detail = originalError && originalError.message ? originalError.message : 'invalid JSON';
@@ -790,10 +817,10 @@ async function startAgent(step, opts = { prompt: '' }) {
790
817
  });
791
818
  tried.add(chosen || '');
792
819
  launched.add(chosen || '');
793
- // Block non-custom agents on non-limit failures so selectAgent excludes them
794
- // on the next retry iteration, and the review-loop fallback path can activate.
795
- // custom (opencode/local AI) is excluded exit 1 is a temporary local error.
796
- if (chosen !== 'custom') {
820
+ // Block retry candidates only when the failure looks transient. Deterministic
821
+ // setup/config errors (invalid model id, auth failure, read-only HOME, etc.)
822
+ // should fall through to the next family without poisoning agents.local.json.
823
+ if (shouldPersistLaunchFailureBlock(chosen || '', result)) {
797
824
  const blockUntil = (0, limit_hit_js_1.formatBlockUntil)(new Date(Date.now() + limit_hit_js_1.DEFAULT_FALLBACK_HOURS * 60 * 60 * 1000));
798
825
  try {
799
826
  const blockResult = updateAgentBlockFn(chosen || '', blockUntil);
@@ -803,6 +830,9 @@ async function startAgent(step, opts = { prompt: '' }) {
803
830
  log(fmt.status('WARN', `Could not persist blocklist entry for ${fmt.agent(chosen || '')}: ${err.message}`));
804
831
  }
805
832
  }
833
+ else {
834
+ log(fmt.status('INFO', `Skipping blocklist write for ${fmt.agent(chosen || '')}; launch failure looks like a deterministic config/setup error.`));
835
+ }
806
836
  chosen = undefined;
807
837
  continue;
808
838
  }
@@ -22,6 +22,14 @@ interface LauncherStatus {
22
22
  reason?: string;
23
23
  }
24
24
 
25
+ interface LaunchResultLike {
26
+ stdout?: string;
27
+ stderr?: string;
28
+ status?: number | null;
29
+ signal?: string | null;
30
+ error?: {code?: string, message?: string} | null;
31
+ }
32
+
25
33
  type AgentConfig = { blocklist?: {[key: string]: any}, steps?: {[key: string]: any} };
26
34
 
27
35
  interface StartAgentOptions {
@@ -100,6 +108,18 @@ const KNOWN_AGENT_NAMES = Object.freeze([
100
108
  'human'
101
109
  ]);
102
110
 
111
+ const NON_BLOCKING_LAUNCH_ERROR_PATTERNS = Object.freeze([
112
+ /\b(?:invalid|unknown|unsupported|unrecognized)\s+model\b/i,
113
+ /\bmodel\s+(?:identifier|id)\s+(?:is\s+)?invalid\b/i,
114
+ /\b(?:model\s+not\s+found|no\s+such\s+model)\b/i,
115
+ /\bunknown\s+option\b/i,
116
+ /\bauth(?:entication)?\b/i,
117
+ /\bunauthorized\b/i,
118
+ /\bforbidden\b/i,
119
+ /\bapi\s+key\b/i,
120
+ /\bread-only file system\b/i
121
+ ]);
122
+
103
123
  function workflowLauncherStatus(agent: string): LauncherStatus {
104
124
  const resolver = RESOLVERS[agent];
105
125
  if (!resolver) {
@@ -146,6 +166,18 @@ function commandInPath(name: string) {
146
166
  return result.status === 0 && result.stdout.trim().length > 0;
147
167
  }
148
168
 
169
+ function shouldPersistLaunchFailureBlock(agent: string, result: LaunchResultLike | null | undefined) {
170
+ if (!result || agent === 'custom') {return false;}
171
+ const combined = [
172
+ result.stderr || '',
173
+ result.stdout || '',
174
+ result.error?.message || '',
175
+ result.error?.code || ''
176
+ ].join('\n');
177
+ if (!combined.trim()) {return true;}
178
+ return !NON_BLOCKING_LAUNCH_ERROR_PATTERNS.some(pattern => pattern.test(combined));
179
+ }
180
+
149
181
  function buildInvalidAgentConfigError(configPath: string, scope: string, originalError: {message?: string} | null) {
150
182
  const location = path.resolve(configPath);
151
183
  const detail = originalError && originalError.message ? originalError.message : 'invalid JSON';
@@ -869,10 +901,10 @@ async function startAgent(step: string, opts: StartAgentOptions = { prompt: '' }
869
901
  });
870
902
  tried.add(chosen || '');
871
903
  launched.add(chosen || '');
872
- // Block non-custom agents on non-limit failures so selectAgent excludes them
873
- // on the next retry iteration, and the review-loop fallback path can activate.
874
- // custom (opencode/local AI) is excluded exit 1 is a temporary local error.
875
- if (chosen !== 'custom') {
904
+ // Block retry candidates only when the failure looks transient. Deterministic
905
+ // setup/config errors (invalid model id, auth failure, read-only HOME, etc.)
906
+ // should fall through to the next family without poisoning agents.local.json.
907
+ if (shouldPersistLaunchFailureBlock(chosen || '', result)) {
876
908
  const blockUntil = formatBlockUntil(new Date(Date.now() + DEFAULT_FALLBACK_HOURS * 60 * 60 * 1000));
877
909
  try {
878
910
  const blockResult = updateAgentBlockFn(chosen || '', blockUntil);
@@ -880,6 +912,8 @@ async function startAgent(step: string, opts: StartAgentOptions = { prompt: '' }
880
912
  } catch (err) {
881
913
  log(fmt.status('WARN', `Could not persist blocklist entry for ${fmt.agent(chosen || '')}: ${(err as any).message}`));
882
914
  }
915
+ } else {
916
+ log(fmt.status('INFO', `Skipping blocklist write for ${fmt.agent(chosen || '')}; launch failure looks like a deterministic config/setup error.`));
883
917
  }
884
918
  chosen = undefined;
885
919
  continue;
@@ -926,5 +960,6 @@ export {
926
960
  isInvalidAgentConfigError,
927
961
  updateAgentBlock,
928
962
  resolveBlocklistTargetPath,
929
- resolveNoOutputWatchdogConfig
963
+ resolveNoOutputWatchdogConfig,
964
+ shouldPersistLaunchFailureBlock
930
965
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magnusekdahl/parallix",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "AI mission workflow toolkit with a px CLI — local-first, human-in-the-loop multi-agent development",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "private": false,