pi-harness-runtime 1.1.18 → 1.1.26

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.
Files changed (101) hide show
  1. package/harness/e2e/auto-test-loop.ts +94 -29
  2. package/harness/e2e/tools-detector.ts +20 -17
  3. package/harness/index.ts +20 -3
  4. package/harness/loop-runtime.ts +49 -13
  5. package/harness/skill-sync.test.ts +57 -0
  6. package/harness/skill-sync.ts +247 -0
  7. package/harness/ssh-hang-recovery.ts +280 -0
  8. package/package.json +1 -1
  9. package/packages/a2a-adapter/package.json +1 -1
  10. package/packages/architecture-generator/package.json +1 -1
  11. package/packages/auth/package.json +1 -1
  12. package/packages/autonomous-refactor/package.json +1 -1
  13. package/packages/autonomous-runtime/package.json +1 -1
  14. package/packages/cache-strategy/package.json +1 -1
  15. package/packages/capability-registry/package.json +1 -1
  16. package/packages/checkpoint/package.json +1 -1
  17. package/packages/cli-plugin-sdk/package.json +1 -1
  18. package/packages/clipboard/package.json +1 -1
  19. package/packages/clipboard-plugin/package.json +1 -1
  20. package/packages/code-generation/package.json +1 -1
  21. package/packages/code-review/package.json +1 -1
  22. package/packages/codex-adapter/package.json +1 -1
  23. package/packages/config-capture/package.json +1 -1
  24. package/packages/context-compiler/package.json +1 -1
  25. package/packages/context-discovery/package.json +1 -1
  26. package/packages/context-manager/package.json +1 -1
  27. package/packages/cookie-sanitizer/package.json +1 -1
  28. package/packages/cost-optimizer/package.json +1 -1
  29. package/packages/dependency-analyzer/package.json +1 -1
  30. package/packages/django-plugin/package.json +1 -1
  31. package/packages/doc-generator/package.json +1 -1
  32. package/packages/evaluation-engine/package.json +1 -1
  33. package/packages/evaluation-runner/package.json +1 -1
  34. package/packages/event-bus/package.json +1 -1
  35. package/packages/event-store/package.json +1 -1
  36. package/packages/experience-replay/package.json +1 -1
  37. package/packages/feedback-collector/package.json +1 -1
  38. package/packages/file-copy-helper/package.json +1 -1
  39. package/packages/framework-detector/package.json +1 -1
  40. package/packages/framework-plugin-sdk/package.json +1 -1
  41. package/packages/frappe-plugin/package.json +1 -1
  42. package/packages/generic-web-plugin/package.json +1 -1
  43. package/packages/health-monitor/package.json +1 -1
  44. package/packages/intent-analyzer/package.json +1 -1
  45. package/packages/knowledge-graph/package.json +1 -1
  46. package/packages/knowledge-retrieval/package.json +1 -1
  47. package/packages/laravel-plugin/package.json +1 -1
  48. package/packages/learning-engine/package.json +1 -1
  49. package/packages/mcp-adapter/package.json +1 -1
  50. package/packages/memory-engine/package.json +1 -1
  51. package/packages/milestone-manager/package.json +1 -1
  52. package/packages/model-registry/package.json +1 -1
  53. package/packages/nextjs-plugin/package.json +1 -1
  54. package/packages/notification/package.json +1 -1
  55. package/packages/observability/package.json +1 -1
  56. package/packages/okf-indexer/package.json +1 -1
  57. package/packages/performance-optimizer/package.json +1 -1
  58. package/packages/privilege-broker/package.json +1 -1
  59. package/packages/project-analyzer/package.json +1 -1
  60. package/packages/project-bootstrap/package.json +1 -1
  61. package/packages/projection-engine/package.json +1 -1
  62. package/packages/prompt-compiler/package.json +1 -1
  63. package/packages/prompt-versioning/package.json +1 -1
  64. package/packages/provider-adapter-sdk/package.json +1 -1
  65. package/packages/provider-router/package.json +1 -1
  66. package/packages/provider-selector/package.json +1 -1
  67. package/packages/providers/package.json +1 -1
  68. package/packages/quota-manager/package.json +1 -1
  69. package/packages/rate-limiter/package.json +1 -1
  70. package/packages/react-vite-plugin/package.json +1 -1
  71. package/packages/release-manager/package.json +1 -1
  72. package/packages/requirement-compiler/package.json +1 -1
  73. package/packages/runtime/package.json +1 -1
  74. package/packages/scheduler/package.json +1 -1
  75. package/packages/scheduler-adapter/package.json +1 -1
  76. package/packages/session/package.json +1 -1
  77. package/packages/session-api/package.json +1 -1
  78. package/packages/session-export/package.json +1 -1
  79. package/packages/shared-context/package.json +1 -1
  80. package/packages/skill-mcp-client/package.json +28 -0
  81. package/packages/skill-mcp-client/src/client.ts +171 -0
  82. package/packages/skill-mcp-client/src/errors.ts +75 -0
  83. package/packages/skill-mcp-client/src/index.ts +13 -0
  84. package/packages/skill-mcp-client/src/types.ts +123 -0
  85. package/packages/skill-registry/package.json +1 -1
  86. package/packages/sprint-planner/package.json +1 -1
  87. package/packages/subscription-engine/package.json +1 -1
  88. package/packages/task-compiler/package.json +1 -1
  89. package/packages/tencentdb-memory/package.json +1 -1
  90. package/packages/tencentdb-sync/package.json +1 -1
  91. package/packages/test-data-generator/package.json +1 -1
  92. package/packages/test-generator/package.json +1 -1
  93. package/packages/todo-bd-sync/package.json +1 -1
  94. package/packages/token-estimation/package.json +1 -1
  95. package/packages/token-optimizer/package.json +1 -1
  96. package/packages/tui/package.json +1 -1
  97. package/packages/types/package.json +1 -1
  98. package/packages/workflow-events/package.json +1 -1
  99. package/packages/workspace-scanner/package.json +1 -1
  100. package/packages/worktree/package.json +1 -1
  101. package/packages/write-review/package.json +1 -1
@@ -20,18 +20,14 @@
20
20
  * Unknown Failure → Retry (max 3x) → Ask Human
21
21
  */
22
22
 
23
- import { existsSync, readdirSync, writeFileSync, readFileSync } from "node:fs";
24
- import { join, extname, dirname } from "node:path";
23
+ import { existsSync, readdirSync, writeFileSync } from "node:fs";
24
+ import { join, extname } from "node:path";
25
25
  import { execSync } from "child_process";
26
26
  import {
27
- detectProjectType,
28
- detectAvailableTools,
29
27
  makeSmartDecision,
30
28
  getE2EToolsConfig,
31
29
  type ProjectType,
32
- type TestingTool,
33
30
  type E2EToolsConfig,
34
- type SmartDecision,
35
31
  } from "./tools-detector.js";
36
32
  import { logInfo, logWarn, logError } from "../glm-quota-logger.js";
37
33
 
@@ -49,6 +45,8 @@ export interface AutoTestConfig {
49
45
  autoFixEnabled: boolean;
50
46
  generateStubs: boolean;
51
47
  notifyOnEscalation: boolean;
48
+ /** Global timeout for entire test run (default: 30 minutes) */
49
+ globalTimeoutMs?: number;
52
50
  }
53
51
 
54
52
  export interface AutoTestResult {
@@ -68,6 +66,7 @@ const DEFAULT_CONFIG: AutoTestConfig = {
68
66
  autoFixEnabled: true,
69
67
  generateStubs: true,
70
68
  notifyOnEscalation: true,
69
+ globalTimeoutMs: 30 * 60 * 1000, // 30 minutes
71
70
  };
72
71
 
73
72
  /**
@@ -95,7 +94,10 @@ export async function runAutoTestLoop(
95
94
  toolsConfig.projectType,
96
95
  toolsConfig.detectedTools,
97
96
  );
98
- logInfo(null, `Smart decision: ${decision.suggestedAction} (confidence: ${decision.confidence})`);
97
+ logInfo(
98
+ null,
99
+ `Smart decision: ${decision.suggestedAction} (confidence: ${decision.confidence})`,
100
+ );
99
101
 
100
102
  // Handle decision
101
103
  switch (decision.suggestedAction) {
@@ -128,7 +130,7 @@ export async function runAutoTestLoop(
128
130
  logInfo(null, "Generating test stubs...");
129
131
  await generateTestStubs(cfg.projectRoot, toolsConfig.projectType);
130
132
  }
131
- // Fall through to run tests
133
+ // Fall through to run tests
132
134
 
133
135
  case "auto_run":
134
136
  default:
@@ -149,6 +151,25 @@ async function runTestLoop(
149
151
  let lastResult: E2ETestResult | null = null;
150
152
 
151
153
  while (retriesAttempted <= cfg.maxRetries) {
154
+ // Check global timeout
155
+ const elapsedMs = Date.now() - startTime;
156
+ if (elapsedMs > (cfg.globalTimeoutMs ?? 30 * 60 * 1000)) {
157
+ logWarn(
158
+ null,
159
+ `⏱️ Global timeout reached (${elapsedMs}ms). Escalating to human.`,
160
+ );
161
+ return {
162
+ success: false,
163
+ testsRun: true,
164
+ testsPassed: false,
165
+ retriesAttempted,
166
+ escalatedToHuman: true,
167
+ escalationReason: `Global timeout reached after ${Math.round(elapsedMs / 1000)}s`,
168
+ output: lastResult?.output || "",
169
+ duration: elapsedMs,
170
+ };
171
+ }
172
+
152
173
  logInfo(
153
174
  null,
154
175
  `Running E2E tests (attempt ${retriesAttempted + 1}/${cfg.maxRetries + 1})...`,
@@ -165,18 +186,28 @@ async function runTestLoop(
165
186
  testsPassed: true,
166
187
  retriesAttempted,
167
188
  escalatedToHuman: false,
168
- output: lastResult && lastResult.output || "",
189
+ output: (lastResult && lastResult.output) || "",
169
190
  duration: Date.now() - startTime,
170
191
  };
171
192
  }
172
193
 
173
194
  // Tests failed
174
- logWarn(null, `❌ E2E tests failed: ${lastResult && lastResult.errorMessage || " error"}`);
195
+ logWarn(
196
+ null,
197
+ `❌ E2E tests failed: ${(lastResult && lastResult.errorMessage) || " error"}`,
198
+ );
175
199
  retriesAttempted++;
176
200
 
177
201
  // Analyze failure
178
- const analysis = analyzeFailure(lastResult!, cfg.projectRoot, toolsConfig.projectType);
179
- logInfo(null, `Failure analysis: ${analysis.canFix ? "Can auto-fix" : "Needs manual intervention"}`);
202
+ const analysis = analyzeFailure(
203
+ lastResult!,
204
+ cfg.projectRoot,
205
+ toolsConfig.projectType,
206
+ );
207
+ logInfo(
208
+ null,
209
+ `Failure analysis: ${analysis.canFix ? "Can auto-fix" : "Needs manual intervention"}`,
210
+ );
180
211
 
181
212
  if (analysis.canFix && cfg.autoFixEnabled) {
182
213
  // Try to fix
@@ -196,8 +227,8 @@ async function runTestLoop(
196
227
  testsPassed: false,
197
228
  retriesAttempted,
198
229
  escalatedToHuman: true,
199
- escalationReason: `Environment limitation: ${lastResult.errorMessage || "Unable to run tests in current environment"}`,
200
- output: lastResult && lastResult.output || "",
230
+ escalationReason: `Environment limitation: ${(lastResult && lastResult.errorMessage) || "Unable to run tests in current environment"}`,
231
+ output: (lastResult && lastResult.output) || "",
201
232
  duration: Date.now() - startTime,
202
233
  };
203
234
  }
@@ -212,7 +243,7 @@ async function runTestLoop(
212
243
  retriesAttempted,
213
244
  escalatedToHuman: true,
214
245
  escalationReason: `Failed after ${cfg.maxRetries} attempts. Last error: ${lastResult?.errorMessage ?? "Unknown"}`,
215
- output: lastResult && lastResult.output || "",
246
+ output: (lastResult && lastResult.output) || "",
216
247
  duration: Date.now() - startTime,
217
248
  };
218
249
  }
@@ -237,7 +268,7 @@ async function runTestLoop(
237
268
  async function runTests(
238
269
  projectRoot: string,
239
270
  toolsConfig: E2EToolsConfig,
240
- ): Promise<TestResult> {
271
+ ): Promise<E2ETestResult> {
241
272
  const startTime = Date.now();
242
273
  const projectType = toolsConfig.projectType;
243
274
 
@@ -264,7 +295,8 @@ async function runTests(
264
295
  output: "",
265
296
  duration: 0,
266
297
  failedTests: [],
267
- errorMessage: "Native app testing requires manual setup. Run tests manually.",
298
+ errorMessage:
299
+ "Native app testing requires manual setup. Run tests manually.",
268
300
  };
269
301
 
270
302
  case "flutter":
@@ -339,15 +371,19 @@ interface FailureAnalysis {
339
371
  */
340
372
  function analyzeFailure(
341
373
  result: E2ETestResult,
342
- projectRoot: string,
343
- projectType: ProjectType,
374
+ _projectRoot: string,
375
+ _projectType: ProjectType,
344
376
  ): FailureAnalysis {
345
377
  const failedTests = result.failedTests || [];
346
378
 
347
379
  // Check for common auto-fixable issues
348
380
 
349
381
  // 1. Missing test file - can generate
350
- if (failedTests.some((t: string) => t.includes("Cannot find") || t.includes("not found"))) {
382
+ if (
383
+ failedTests.some(
384
+ (t: string) => t.includes("Cannot find") || t.includes("not found"),
385
+ )
386
+ ) {
351
387
  return {
352
388
  canFix: true,
353
389
  reason: "Missing test file - can generate stubs",
@@ -358,7 +394,10 @@ function analyzeFailure(
358
394
  }
359
395
 
360
396
  // 2. Import errors - can fix
361
- if (result.output.includes("Cannot find module") || result.output.includes("import error")) {
397
+ if (
398
+ result.output.includes("Cannot find module") ||
399
+ result.output.includes("import error")
400
+ ) {
362
401
  return {
363
402
  canFix: true,
364
403
  reason: "Import error - may need npm install",
@@ -369,7 +408,11 @@ function analyzeFailure(
369
408
  }
370
409
 
371
410
  // 3. Locator errors - can fix (re-generate selectors)
372
- if (failedTests.some((t: string) => t.includes("locator") || t.includes("selector"))) {
411
+ if (
412
+ failedTests.some(
413
+ (t: string) => t.includes("locator") || t.includes("selector"),
414
+ )
415
+ ) {
373
416
  return {
374
417
  canFix: true,
375
418
  reason: "UI locator changed - need to update selectors",
@@ -380,7 +423,12 @@ function analyzeFailure(
380
423
  }
381
424
 
382
425
  // 4. Auth/session issues - might be environment
383
- if (failedTests.some((t: string) => t.includes("auth") || t.includes("login") || t.includes("session"))) {
426
+ if (
427
+ failedTests.some(
428
+ (t: string) =>
429
+ t.includes("auth") || t.includes("login") || t.includes("session"),
430
+ )
431
+ ) {
384
432
  return {
385
433
  canFix: false,
386
434
  reason: "Authentication issue - may need test credentials",
@@ -438,7 +486,10 @@ async function attemptAutoFix(
438
486
  }
439
487
 
440
488
  // Fix 2: Run npm install
441
- if (analysis.reason.includes("import error") || analysis.reason.includes("Missing dependencies")) {
489
+ if (
490
+ analysis.reason.includes("import error") ||
491
+ analysis.reason.includes("Missing dependencies")
492
+ ) {
442
493
  try {
443
494
  execSync("npm install", { cwd: projectRoot, stdio: "ignore" });
444
495
  return { fixed: true, description: "Ran npm install" };
@@ -448,7 +499,10 @@ async function attemptAutoFix(
448
499
  }
449
500
 
450
501
  // Fix 3: Start dev server (for web apps)
451
- if (analysis.reason.includes("Timeout") || analysis.reason.includes("Server slow")) {
502
+ if (
503
+ analysis.reason.includes("Timeout") ||
504
+ analysis.reason.includes("Server slow")
505
+ ) {
452
506
  try {
453
507
  // Check if dev server is already running
454
508
  const port = 3000;
@@ -498,7 +552,9 @@ function isEnvironmentLimitation(result: E2ETestResult): boolean {
498
552
  "ETIMEDOUT",
499
553
  ];
500
554
 
501
- return envIssues.some((issue) => output.toLowerCase().includes(issue.toLowerCase()));
555
+ return envIssues.some((issue) =>
556
+ output.toLowerCase().includes(issue.toLowerCase()),
557
+ );
502
558
  }
503
559
 
504
560
  /**
@@ -630,7 +686,10 @@ function findSourceFiles(srcDir: string, projectType: ProjectType): string[] {
630
686
  generic: [".ts", ".tsx", ".js", ".jsx"],
631
687
  };
632
688
 
633
- const exts = extensions[projectType as keyof typeof extensions] || [".ts", ".tsx"];
689
+ const exts = extensions[projectType as keyof typeof extensions] || [
690
+ ".ts",
691
+ ".tsx",
692
+ ];
634
693
  const files: string[] = [];
635
694
 
636
695
  try {
@@ -666,13 +725,19 @@ function createTestFilePath(
666
725
  generic: ".test.ts",
667
726
  };
668
727
 
669
- return join(testDir, `${base}${ext[projectType as keyof typeof ext] || ".test.ts"}`);
728
+ return join(
729
+ testDir,
730
+ `${base}${ext[projectType as keyof typeof ext] || ".test.ts"}`,
731
+ );
670
732
  }
671
733
 
672
734
  /**
673
735
  * Generate basic test content
674
736
  */
675
- function generateTestContent(sourceFile: string, projectType: ProjectType): string {
737
+ function generateTestContent(
738
+ sourceFile: string,
739
+ projectType: ProjectType,
740
+ ): string {
676
741
  const base = require("path").basename(sourceFile, extname(sourceFile));
677
742
 
678
743
  switch (projectType) {
@@ -27,8 +27,18 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
27
27
  import { join, extname } from "node:path";
28
28
  import { homedir } from "node:os";
29
29
 
30
- export type NativeAppType = "tauri" | "ios" | "android" | "electron" | "flutter";
31
- export type WebFramework = "nextjs" | "react_vite" | "django" | "laravel" | "generic";
30
+ export type NativeAppType =
31
+ | "tauri"
32
+ | "ios"
33
+ | "android"
34
+ | "electron"
35
+ | "flutter";
36
+ export type WebFramework =
37
+ | "nextjs"
38
+ | "react_vite"
39
+ | "django"
40
+ | "laravel"
41
+ | "generic";
32
42
  export type ProjectType = NativeAppType | WebFramework | "unknown";
33
43
 
34
44
  export interface TestingTool {
@@ -195,11 +205,7 @@ const TESTING_TOOLS_MAP: Record<
195
205
  manualTools: [],
196
206
  },
197
207
  flutter: {
198
- detectionSignals: [
199
- "pubspec.yaml",
200
- "lib/main.dart",
201
- "test/*.dart",
202
- ],
208
+ detectionSignals: ["pubspec.yaml", "lib/main.dart", "test/*.dart"],
203
209
  autoTools: [
204
210
  {
205
211
  name: "Flutter Test",
@@ -411,10 +417,7 @@ export function detectProjectType(projectRoot: string): ProjectType {
411
417
  }
412
418
 
413
419
  // Check for Django
414
- if (
415
- hasFile(projectRoot, "manage.py") ||
416
- hasFile(projectRoot, "settings.py")
417
- ) {
420
+ if (hasFile(projectRoot, "manage.py") || hasFile(projectRoot, "settings.py")) {
418
421
  return "django";
419
422
  }
420
423
 
@@ -583,16 +586,15 @@ export function makeSmartDecision(
583
586
  if (["django", "laravel"].includes(projectType)) {
584
587
  const hasFrameworkTest =
585
588
  installedAutoTools.some(
586
- (t) =>
587
- t.name.includes("Django") || t.name.includes("Laravel Dusk"),
588
- ) ||
589
- installedAutoTools.some((t) => t.name.includes("Playwright"));
589
+ (t) => t.name.includes("Django") || t.name.includes("Laravel Dusk"),
590
+ ) || installedAutoTools.some((t) => t.name.includes("Playwright"));
590
591
 
591
592
  if (!hasFrameworkTest) {
592
593
  return {
593
594
  shouldAutoTest: false,
594
595
  confidence: 0.3,
595
- reason: "Backend framework detected. Install framework-specific test tools.",
596
+ reason:
597
+ "Backend framework detected. Install framework-specific test tools.",
596
598
  suggestedAction: "ask_human",
597
599
  };
598
600
  }
@@ -609,7 +611,8 @@ export function makeSmartDecision(
609
611
  return {
610
612
  shouldAutoTest: false,
611
613
  confidence: 0.5,
612
- reason: "Backend project detected. Auto-tests skipped (manual review needed).",
614
+ reason:
615
+ "Backend project detected. Auto-tests skipped (manual review needed).",
613
616
  suggestedAction: "skip",
614
617
  };
615
618
  }
package/harness/index.ts CHANGED
@@ -278,11 +278,28 @@ export type {
278
278
  ProjectType,
279
279
  NativeAppType,
280
280
  } from "./e2e/tools-detector.js";
281
- export {
282
- runAutoTestLoop,
283
- } from "./e2e/auto-test-loop.js";
281
+ export { runAutoTestLoop } from "./e2e/auto-test-loop.js";
284
282
  export type {
285
283
  E2ETestResult,
286
284
  AutoTestConfig,
287
285
  AutoTestResult,
288
286
  } from "./e2e/auto-test-loop.js";
287
+
288
+ // --- Skill SaaS Sync (RFC-0106) ----------------------------------------
289
+ export {
290
+ syncSkillsFromSaaS,
291
+ getSkillFromSaaS,
292
+ isSkillSyncConfigured,
293
+ getSyncStatus,
294
+ } from "./skill-sync.js";
295
+
296
+ // --- SSH Hang Recovery (auto-recovery from SSH hangs) -------------------
297
+ export {
298
+ sshWithRecovery,
299
+ pm2Restart,
300
+ getPM2Status,
301
+ safeSSHCommand,
302
+ pingServer,
303
+ type SSHConfig,
304
+ type SSHResult,
305
+ } from "./ssh-hang-recovery.js";
@@ -46,6 +46,9 @@ import {
46
46
  createForkedSummarizer,
47
47
  } from "./forked-summarizer.js";
48
48
 
49
+ // GLM quota error parsing for 429 responses
50
+ import { parseGLMErrorResetTime } from "./e2e/glm-quota-scraper.js";
51
+
49
52
  // --- Constants ----------------------------------------------------------------
50
53
 
51
54
  const DEFAULT_MAX_COMPACT_RETRIES = 3;
@@ -84,9 +87,7 @@ export interface LoopCallbacks {
84
87
  /** Get current checkpoint (used by auto-resume to read resumeAt) */
85
88
  onGetCheckpoint?: () => Promise<RuntimeCheckpoint | null>;
86
89
  /** Check mirror for a provider (used by auto-resume to detect early reset) */
87
- onCheckMirror?: (
88
- provider: string,
89
- ) => Promise<{ h5_used_pct?: number } | null>;
90
+ onCheckMirror?: (provider: string) => Promise<{ h5_used_pct?: number } | null>;
90
91
  /** Generic notify (used by auto-resume to tell user wait time) */
91
92
  onNotify?: (msg: string, type?: string) => Promise<void>;
92
93
  /** Save checkpoint (call persistence layer) */
@@ -244,11 +245,7 @@ export class LoopRuntime {
244
245
  await new Promise((r) => setTimeout(r, 5 * 60_000)); // 5 min
245
246
  // Re-check mirror — quota may have reset early
246
247
  const fresh = await this.callbacks.onCheckMirror?.("minimax");
247
- if (
248
- fresh &&
249
- fresh.h5_used_pct !== undefined &&
250
- fresh.h5_used_pct < 100
251
- ) {
248
+ if (fresh && fresh.h5_used_pct !== undefined && fresh.h5_used_pct < 100) {
252
249
  break; // quota reset early, resume now
253
250
  }
254
251
  }
@@ -447,14 +444,54 @@ export class LoopRuntime {
447
444
  continue;
448
445
  }
449
446
 
450
- // Error without compact — surface it
451
- throw new Error(result.error ?? "Unknown invoke error");
447
+ // Error without compact — check for GLM 429 quota error
448
+ const errMsg = result.error ?? "Unknown invoke error";
449
+ if (errMsg.includes("1308") || errMsg.includes("Usage limit")) {
450
+ const resetTime = parseGLMErrorResetTime(errMsg);
451
+ if (resetTime) {
452
+ const resetEpoch = new Date(resetTime).getTime();
453
+ this.mirrorStore.writeProvider("minimax", {
454
+ provider: "minimax",
455
+ synced_at: new Date().toISOString(),
456
+ source: "tui-signal",
457
+ h5_used_pct: 100,
458
+ h5_resets_at_epoch: resetEpoch,
459
+ });
460
+ console.log(`[LoopRuntime] GLM quota hit, reset at ${resetTime}`);
461
+ }
462
+ await this.handleQuotaPause();
463
+ return;
464
+ }
465
+ throw new Error(errMsg);
452
466
  }
453
467
 
454
468
  // Fallback: direct invoke without orchestrator
455
469
  const result = await this.callbacks.onInvokeAgent?.(invokeOptions);
456
470
  if (!result || !result.success) {
457
- throw new Error(result?.error ?? "Invoke failed");
471
+ const errorMsg = result?.error ?? "Invoke failed";
472
+
473
+ // Check if this is a GLM 429 quota error with reset time
474
+ if (errorMsg.includes("1308") || errorMsg.includes("Usage limit")) {
475
+ const resetTime = parseGLMErrorResetTime(errorMsg);
476
+ if (resetTime) {
477
+ // Update mirror with reset epoch so auto-resume works
478
+ const resetEpoch = new Date(resetTime).getTime();
479
+ this.mirrorStore.writeProvider("minimax", {
480
+ provider: "minimax",
481
+ synced_at: new Date().toISOString(),
482
+ source: "tui-signal",
483
+ h5_used_pct: 100,
484
+ h5_resets_at_epoch: resetEpoch,
485
+ });
486
+ console.log(`[LoopRuntime] GLM quota hit, reset at ${resetTime}`);
487
+ }
488
+
489
+ // Handle quota pause instead of throwing
490
+ await this.handleQuotaPause();
491
+ return;
492
+ }
493
+
494
+ throw new Error(errorMsg);
458
495
  }
459
496
 
460
497
  const continued = await this.handleAutoContinuation(
@@ -660,8 +697,7 @@ export class LoopRuntime {
660
697
  // Append repair instruction
661
698
  messages.push({
662
699
  role: "user",
663
- content:
664
- "Tests failed. Please fix the issues and ensure all tests pass.",
700
+ content: "Tests failed. Please fix the issues and ensure all tests pass.",
665
701
  timestamp: Date.now(),
666
702
  });
667
703
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Skill Sync Tests
3
+ */
4
+
5
+ // @ts-ignore - bun:test types
6
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
7
+ import { tmpdir } from "node:os";
8
+ import { mkdirSync, rmSync } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ // Mock the environment
12
+ const originalApiKey = process.env.SKILLS_SAAS_API_KEY;
13
+
14
+ describe("Skill Sync", () => {
15
+ let tempCacheDir: string;
16
+
17
+ beforeEach(() => {
18
+ tempCacheDir = join(tmpdir(), `skill-sync-test-${Date.now()}`);
19
+ mkdirSync(tempCacheDir, { recursive: true });
20
+ });
21
+
22
+ afterEach(() => {
23
+ // Restore env
24
+ if (originalApiKey === undefined) {
25
+ delete process.env.SKILLS_SAAS_API_KEY;
26
+ } else {
27
+ process.env.SKILLS_SAAS_API_KEY = originalApiKey;
28
+ }
29
+ // Cleanup
30
+ try {
31
+ rmSync(tempCacheDir, { recursive: true });
32
+ } catch {
33
+ // Ignore
34
+ }
35
+ });
36
+
37
+ describe("isSkillSyncConfigured", () => {
38
+ test("returns false when API key not set", () => {
39
+ delete process.env.SKILLS_SAAS_API_KEY;
40
+ // Import dynamically to test
41
+ expect(process.env.SKILLS_SAAS_API_KEY).toBeUndefined();
42
+ });
43
+
44
+ test("returns true when API key is set", () => {
45
+ process.env.SKILLS_SAAS_API_KEY = "sk_test_xxx";
46
+ expect(process.env.SKILLS_SAAS_API_KEY).toBe("sk_test_xxx");
47
+ });
48
+ });
49
+
50
+ describe("getSyncStatus", () => {
51
+ test("returns correct status when not configured", () => {
52
+ delete process.env.SKILLS_SAAS_API_KEY;
53
+ // Status should show not configured
54
+ expect(process.env.SKILLS_SAAS_API_KEY).toBeUndefined();
55
+ });
56
+ });
57
+ });