pi-harness-runtime 1.1.49 → 1.1.71-3
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.
- package/CHANGELOG.md +30 -9
- package/README.md +119 -0
- package/harness/langchain/agents.d.ts +143 -0
- package/harness/langchain/agents.test.d.ts +2 -0
- package/harness/langchain/agents.test.ts +0 -1
- package/harness/langchain/agents.ts +196 -13
- package/harness/langchain/checkpointer.d.ts +48 -0
- package/harness/langchain/checkpointer.test.d.ts +8 -0
- package/harness/langchain/cron-cli.d.ts +12 -0
- package/harness/langchain/cron-cli.ts +169 -0
- package/harness/langchain/daemon.d.ts +161 -0
- package/harness/langchain/daemon.test.d.ts +11 -0
- package/harness/langchain/daemon.test.ts +62 -5
- package/harness/langchain/daemon.ts +231 -12
- package/harness/langchain/file-tools.d.ts +97 -0
- package/harness/langchain/file-tools.ts +29 -11
- package/harness/langchain/graph.d.ts +706 -0
- package/harness/langchain/graph.ts +184 -26
- package/harness/langchain/model-router.d.ts +63 -0
- package/harness/langchain/model-router.test.d.ts +2 -0
- package/harness/langchain/model-router.test.ts +191 -0
- package/harness/langchain/model-router.ts +188 -0
- package/harness/langchain/models.d.ts +72 -0
- package/harness/langchain/models.ts +111 -49
- package/harness/langchain/ping-pong-middleware.d.ts +28 -0
- package/harness/langchain/ping-pong-middleware.test.d.ts +2 -0
- package/harness/langchain/ping-pong-middleware.test.ts +69 -0
- package/harness/langchain/ping-pong-middleware.ts +204 -0
- package/harness/langchain/run.d.ts +15 -0
- package/harness/langchain/run.ts +15 -3
- package/harness/langchain/scoreboard.test.d.ts +13 -0
- package/harness/langchain/scoreboard.test.ts +28 -5
- package/harness/langchain/status-line.d.ts +91 -0
- package/harness/langchain/status-line.test.d.ts +8 -0
- package/harness/langchain/surge.d.ts +103 -0
- package/harness/langchain/surge.test.d.ts +10 -0
- package/harness/langchain/surge.test.ts +179 -0
- package/harness/langchain/surge.ts +188 -19
- package/harness/langchain/widget.d.ts +105 -0
- package/harness/langchain/widget.test.d.ts +8 -0
- package/harness/ssh-detach-interceptor.test.ts +263 -0
- package/harness/ssh-detach-interceptor.ts +438 -0
- package/index.ts +212 -47
- package/package.json +4 -2
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/dist/src/index.d.ts +2 -0
- package/packages/intent-analyzer/dist/src/index.d.ts.map +1 -1
- package/packages/intent-analyzer/dist/src/index.js +1 -0
- package/packages/intent-analyzer/dist/src/index.js.map +1 -1
- package/packages/intent-analyzer/dist/src/ping-pong.d.ts +56 -0
- package/packages/intent-analyzer/dist/src/ping-pong.d.ts.map +1 -0
- package/packages/intent-analyzer/dist/src/ping-pong.js +286 -0
- package/packages/intent-analyzer/dist/src/ping-pong.js.map +1 -0
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/intent-analyzer/src/index.ts +13 -0
- package/packages/intent-analyzer/src/ping-pong.ts +387 -0
- package/packages/intent-analyzer/test/ping-pong.test.ts +96 -0
- package/packages/jev-judge/README.md +234 -0
- package/packages/jev-judge/dist/decision-engine.d.ts +100 -0
- package/packages/jev-judge/dist/decision-engine.d.ts.map +1 -0
- package/packages/jev-judge/dist/decision-engine.js +151 -0
- package/packages/jev-judge/dist/decision-engine.js.map +1 -0
- package/packages/jev-judge/dist/e2e-judge.d.ts +61 -0
- package/packages/jev-judge/dist/e2e-judge.d.ts.map +1 -0
- package/packages/jev-judge/dist/e2e-judge.js +268 -0
- package/packages/jev-judge/dist/e2e-judge.js.map +1 -0
- package/packages/jev-judge/dist/flaky-detector.d.ts +34 -0
- package/packages/jev-judge/dist/flaky-detector.d.ts.map +1 -0
- package/packages/jev-judge/dist/flaky-detector.js +154 -0
- package/packages/jev-judge/dist/flaky-detector.js.map +1 -0
- package/packages/jev-judge/dist/index.d.ts +77 -0
- package/packages/jev-judge/dist/index.d.ts.map +1 -0
- package/packages/jev-judge/dist/index.js +271 -0
- package/packages/jev-judge/dist/index.js.map +1 -0
- package/packages/jev-judge/dist/types.d.ts +97 -0
- package/packages/jev-judge/dist/types.d.ts.map +1 -0
- package/packages/jev-judge/dist/types.js +8 -0
- package/packages/jev-judge/dist/types.js.map +1 -0
- package/packages/jev-judge/package.json +46 -0
- package/packages/jev-judge/src/decision-engine.ts +231 -0
- package/packages/jev-judge/src/e2e-judge.ts +364 -0
- package/packages/jev-judge/src/flaky-detector.ts +190 -0
- package/packages/jev-judge/src/index.ts +345 -0
- package/packages/jev-judge/src/types.ts +131 -0
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/README.md +71 -0
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/todo-bd-sync/src/extension.ts +16 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/dist/src/approved-patterns.d.ts +51 -0
- package/packages/trajectory/dist/src/approved-patterns.d.ts.map +1 -0
- package/packages/trajectory/dist/src/approved-patterns.js +134 -0
- package/packages/trajectory/dist/src/approved-patterns.js.map +1 -0
- package/packages/trajectory/dist/src/index.d.ts +13 -0
- package/packages/trajectory/dist/src/index.d.ts.map +1 -0
- package/packages/trajectory/dist/src/index.js +12 -0
- package/packages/trajectory/dist/src/index.js.map +1 -0
- package/packages/trajectory/dist/src/store.d.ts +69 -0
- package/packages/trajectory/dist/src/store.d.ts.map +1 -0
- package/packages/trajectory/dist/src/store.js +253 -0
- package/packages/trajectory/dist/src/store.js.map +1 -0
- package/packages/trajectory/dist/src/types.d.ts +73 -0
- package/packages/trajectory/dist/src/types.d.ts.map +1 -0
- package/packages/trajectory/dist/src/types.js +9 -0
- package/packages/trajectory/dist/src/types.js.map +1 -0
- package/packages/trajectory/dist/test/store.test.d.ts +2 -0
- package/packages/trajectory/dist/test/store.test.d.ts.map +1 -0
- package/packages/trajectory/dist/test/store.test.js +228 -0
- package/packages/trajectory/dist/test/store.test.js.map +1 -0
- package/packages/trajectory/package.json +29 -0
- package/packages/trajectory/src/approved-patterns.ts +180 -0
- package/packages/trajectory/src/index.ts +26 -0
- package/packages/trajectory/src/store.ts +285 -0
- package/packages/trajectory/src/types.ts +84 -0
- package/packages/trajectory/test/store.test.d.ts +2 -0
- package/packages/trajectory/test/store.test.ts +250 -0
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/packages/write-review/src/blackboard.ts +38 -5
- package/packages/write-review/src/types.ts +13 -0
- package/skills/harness-runtime/SKILL.md +67 -4
|
@@ -212,3 +212,182 @@ describe("T8 — loop deps surviving a mid-graph 529", () => {
|
|
|
212
212
|
assert.deepEqual(sleeps, [100]);
|
|
213
213
|
});
|
|
214
214
|
});
|
|
215
|
+
|
|
216
|
+
// ─── GLM Quota Tests ─────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
import { classifyGLMQuota, invokeWithGLMRetry } from "./surge.js";
|
|
219
|
+
|
|
220
|
+
describe("classifyGLMQuota", () => {
|
|
221
|
+
// The dates in the error messages are in the past (2026-09-04).
|
|
222
|
+
// classifyGLMQuota shifts them to the NEXT occurrence (+1 day).
|
|
223
|
+
// The tests verify: shift happened + time preserved verbatim.
|
|
224
|
+
it("parses GLM 1308 error with reset timestamp", () => {
|
|
225
|
+
const err = new Error(
|
|
226
|
+
'429: {"code":"1308","message":"Usage limit reached for 5 hour. Your limit will reset at 2026-09-04 20:29:24"}',
|
|
227
|
+
);
|
|
228
|
+
const sig = classifyGLMQuota(err);
|
|
229
|
+
assert.ok(sig, "should be classified as GLM quota");
|
|
230
|
+
// 2026-09-04 is past → shifted to 2026-09-05; epoch must be in the future
|
|
231
|
+
assert.ok(sig!.resetAtEpoch > Date.now(), "resetAtEpoch is in the future");
|
|
232
|
+
assert.ok(
|
|
233
|
+
sig!.resetAt.includes("2026-09-05"),
|
|
234
|
+
"date shifted to next occurrence",
|
|
235
|
+
);
|
|
236
|
+
assert.ok(sig!.resetAt.includes("20:29:24"), "time preserved verbatim");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("parses 1308 from plain string error", () => {
|
|
240
|
+
const err =
|
|
241
|
+
'{"code":"1308","message":"Usage limit reached. Your limit will reset at 2026-09-04 20:29:24"}';
|
|
242
|
+
const sig = classifyGLMQuota(err);
|
|
243
|
+
assert.ok(sig, "should be classified as GLM quota");
|
|
244
|
+
// space separator → normalised to T; past date → shifted to next occurrence
|
|
245
|
+
assert.ok(
|
|
246
|
+
sig!.resetAt.includes("2026-09-05"),
|
|
247
|
+
"space normalised and date shifted",
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("parses reset time with T separator", () => {
|
|
252
|
+
const err = new Error(
|
|
253
|
+
'{"code":"1308","message":"Usage limit reached. Your limit will reset at 2026-09-04T20:29:24"}',
|
|
254
|
+
);
|
|
255
|
+
const sig = classifyGLMQuota(err);
|
|
256
|
+
assert.ok(sig, "should be classified as GLM quota");
|
|
257
|
+
// past date → next occurrence (2026-09-05); T separator preserved verbatim
|
|
258
|
+
assert.ok(
|
|
259
|
+
sig!.resetAt.includes("2026-09-05"),
|
|
260
|
+
"date shifted to next occurrence",
|
|
261
|
+
);
|
|
262
|
+
assert.ok(sig!.resetAt.includes("T"), "T separator preserved verbatim");
|
|
263
|
+
assert.ok(sig!.resetAt.includes("20:29:24"), "time preserved verbatim");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("returns null for non-GLM errors", () => {
|
|
267
|
+
const err = new Error("529: overloaded error");
|
|
268
|
+
assert.equal(classifyGLMQuota(err), null);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("returns null for plain errors without 1308", () => {
|
|
272
|
+
const err = new Error("Something went wrong");
|
|
273
|
+
assert.equal(classifyGLMQuota(err), null);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("1308 without reset time uses 5-min fallback", () => {
|
|
277
|
+
const err = new Error('{"code":"1308","message":"Usage limit reached"}');
|
|
278
|
+
const sig = classifyGLMQuota(err);
|
|
279
|
+
assert.ok(sig, "should be classified");
|
|
280
|
+
const diffMs = sig!.resetAtEpoch - Date.now();
|
|
281
|
+
// Should be approximately 5 minutes
|
|
282
|
+
assert.ok(diffMs >= 4 * 60_000 && diffMs <= 6 * 60_000, "5-min fallback");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("invokeWithGLMRetry", () => {
|
|
287
|
+
it("succeeds immediately when fn succeeds", async () => {
|
|
288
|
+
const result = await invokeWithGLMRetry(() => Promise.resolve("ok"));
|
|
289
|
+
assert.equal(result, "ok");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("throws non-GLM errors immediately", async () => {
|
|
293
|
+
await assert.rejects(
|
|
294
|
+
() =>
|
|
295
|
+
invokeWithGLMRetry(() => {
|
|
296
|
+
throw new Error("not a GLM error");
|
|
297
|
+
}),
|
|
298
|
+
/not a GLM error/,
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("waits until reset time before retry", async () => {
|
|
303
|
+
const sleeps: number[] = [];
|
|
304
|
+
const sleep = async (ms: number) => void sleeps.push(ms);
|
|
305
|
+
// Use a reset time 100ms in the future
|
|
306
|
+
const resetIn = 100;
|
|
307
|
+
const resetAt = new Date(Date.now() + resetIn).toISOString();
|
|
308
|
+
|
|
309
|
+
let attempts = 0;
|
|
310
|
+
const result = await invokeWithGLMRetry(
|
|
311
|
+
async () => {
|
|
312
|
+
attempts++;
|
|
313
|
+
if (attempts === 1) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`{"code":"1308","message":"Usage limit. Reset at ${resetAt}"}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return "success after retry";
|
|
319
|
+
},
|
|
320
|
+
{ sleep, tickMs: 10 },
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
assert.equal(result, "success after retry");
|
|
324
|
+
assert.equal(attempts, 2, "retried after wait");
|
|
325
|
+
assert.ok(sleeps.length >= 1, "slept at least once during countdown");
|
|
326
|
+
assert.ok(
|
|
327
|
+
sleeps.every((ms) => ms === 10),
|
|
328
|
+
"tick interval used",
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("fires onGLMQuota callback during countdown", async () => {
|
|
333
|
+
const ticks: string[] = [];
|
|
334
|
+
const sleep = async (ms: number) => void ticks.push(`sleep:${ms}`);
|
|
335
|
+
const resetAt = new Date(Date.now() + 50).toISOString();
|
|
336
|
+
|
|
337
|
+
let attempts = 0;
|
|
338
|
+
await invokeWithGLMRetry(
|
|
339
|
+
async () => {
|
|
340
|
+
attempts++;
|
|
341
|
+
if (attempts === 1) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`{"code":"1308","message":"Usage limit. Reset at ${resetAt}"}`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return "done";
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
sleep,
|
|
350
|
+
tickMs: 10,
|
|
351
|
+
onGLMQuota: (sig) => ticks.push(`quota:${sig.resetAtEpoch}`),
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
assert.ok(
|
|
356
|
+
ticks.some((t) => t.startsWith("quota:")),
|
|
357
|
+
"onGLMQuota was called at least once",
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("fires onGLMRetry callback after countdown expires", async () => {
|
|
362
|
+
const events: string[] = [];
|
|
363
|
+
const sleep = async (_ms: number) => void null;
|
|
364
|
+
const resetAt = new Date(Date.now() + 20).toISOString();
|
|
365
|
+
|
|
366
|
+
let attempts = 0;
|
|
367
|
+
await invokeWithGLMRetry(
|
|
368
|
+
async () => {
|
|
369
|
+
attempts++;
|
|
370
|
+
if (attempts === 1) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`{"code":"1308","message":"Usage limit. Reset at ${resetAt}"}`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return "done";
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
sleep,
|
|
379
|
+
tickMs: 10,
|
|
380
|
+
onGLMQuota: () => events.push("quota"),
|
|
381
|
+
onGLMRetry: () => events.push("retry"),
|
|
382
|
+
},
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
assert.ok(events.includes("quota"), "quota fired");
|
|
386
|
+
assert.ok(events.includes("retry"), "retry fired");
|
|
387
|
+
// retry fires AFTER quota
|
|
388
|
+
assert.ok(
|
|
389
|
+
events.indexOf("retry") > events.indexOf("quota"),
|
|
390
|
+
"retry after quota",
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Surge auto-resume — 529 / overloaded_error
|
|
3
|
-
* and
|
|
2
|
+
* Surge auto-resume — 529 / overloaded_error (MiniMax) + 1308 (GLM quota) detection,
|
|
3
|
+
* escalation backoff, and retry wrappers for the daemon's graph invocations.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Verdict vocabulary and patterns intentionally match:
|
|
10
|
-
* - harness/loop-runtime.ts 529 path (commit 9ae0ffd)
|
|
11
|
-
* - harness/e2e/glm-quota-scraper.ts parseMinimaxOverloadResetTime()
|
|
12
|
-
* - packages/providers/src/adapters.ts isOverloaded
|
|
5
|
+
* Two wrappers:
|
|
6
|
+
* invokeWithSurgeRetry — MiniMax 529/overload; exponential backoff
|
|
7
|
+
* invokeWithGLMRetry — GLM 1308 quota; waits until reset time
|
|
13
8
|
*
|
|
14
9
|
* Wiki: wiki/peak-hour-surge-auto-resume.md
|
|
15
10
|
*/
|
|
@@ -37,7 +32,7 @@ const SIMPLE_DELAY = /retry (?:after|in) (\d+)\s*(seconds?|secs?|s)\b/i;
|
|
|
37
32
|
const RETRY_AFTER_MS = /retry.?after[^0-9]{0,12}(\d{4,})\s*ms/i;
|
|
38
33
|
|
|
39
34
|
/**
|
|
40
|
-
* Classify an unknown error as a transient provider surge.
|
|
35
|
+
* Classify an unknown error as a transient provider surge (MiniMax).
|
|
41
36
|
* Returns null for anything that is not a 529/overload-class failure.
|
|
42
37
|
*/
|
|
43
38
|
export function classifySurge(err: unknown): SurgeSignal | null {
|
|
@@ -92,6 +87,87 @@ export function classifySurge(err: unknown): SurgeSignal | null {
|
|
|
92
87
|
return { retryAfterMs: 120_000, explicit: false, sourceText };
|
|
93
88
|
}
|
|
94
89
|
|
|
90
|
+
// ─── GLM Quota Classification ─────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/** Matches the reset-at timestamp from a GLM 1308 error. */
|
|
93
|
+
const GLM_RESET_AT = /reset at (\d{4}-\d{2}-\d{2}[T ]?\d{2}:\d{2}:\d{2})/i;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Add 1 calendar day to an ISO date string "YYYY-MM-DDTHH:MM:SS".
|
|
97
|
+
* Used to shift the reset date when it has already passed.
|
|
98
|
+
*/
|
|
99
|
+
function _shiftDateByOneDay(isoDateTime: string): string {
|
|
100
|
+
const d = new Date(isoDateTime + "Z");
|
|
101
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
102
|
+
// Re-format: YYYY-MM-DDTHH:MM:SS (always 19 chars, seconds included)
|
|
103
|
+
const yyyy = d.getUTCFullYear();
|
|
104
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
105
|
+
const dd = String(d.getUTCDate()).padStart(2, "0");
|
|
106
|
+
// Preserve the time part from the original string
|
|
107
|
+
const timePart = isoDateTime.slice(11); // "HH:MM:SS"
|
|
108
|
+
return `${yyyy}-${mm}-${dd}T${timePart}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Classify a GLM 1308 quota exhaustion error.
|
|
113
|
+
* Returns the reset-at ISO timestamp, or null if not a GLM quota error.
|
|
114
|
+
*/
|
|
115
|
+
export function classifyGLMQuota(err: unknown): GLMQuotaSignal | null {
|
|
116
|
+
const sourceText =
|
|
117
|
+
err instanceof Error
|
|
118
|
+
? err.message
|
|
119
|
+
: typeof err === "string"
|
|
120
|
+
? err
|
|
121
|
+
: String(err);
|
|
122
|
+
|
|
123
|
+
// Detect GLM 1308 — quota exhaustion
|
|
124
|
+
const has1308 = /"code"\s*:\s*"1308"|code.*1308/i.test(sourceText);
|
|
125
|
+
const hasReset = GLM_RESET_AT.test(sourceText);
|
|
126
|
+
if (!has1308 && !hasReset) return null;
|
|
127
|
+
|
|
128
|
+
// Extract reset timestamp
|
|
129
|
+
const match = sourceText.match(GLM_RESET_AT);
|
|
130
|
+
if (!match) {
|
|
131
|
+
// 1308 without a reset time — use a conservative default (5 min)
|
|
132
|
+
return {
|
|
133
|
+
resetAt: new Date(Date.now() + 5 * 60_000).toISOString(),
|
|
134
|
+
resetAtEpoch: Date.now() + 5 * 60_000,
|
|
135
|
+
sourceText,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Normalise: space → T, then ensure HH:MM:SS (append :00 if only HH:MM)
|
|
140
|
+
const normalised = match[1].replace(" ", "T");
|
|
141
|
+
// "2026-09-04T20:29:24" (19 chars, with seconds) → use as-is
|
|
142
|
+
// "2026-09-04T20:29" (16 chars, no seconds) → append :00
|
|
143
|
+
const resetAt = normalised.length === 16 ? `${normalised}:00` : normalised;
|
|
144
|
+
// Parse as UTC so the date string is preserved exactly (no TZ shift).
|
|
145
|
+
const resetAtEpoch = new Date(resetAt + "Z").getTime();
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
|
|
148
|
+
// If the parsed time is in the past, the date is already "next occurrence".
|
|
149
|
+
// Preserve the original resetAt string so callers can include it verbatim.
|
|
150
|
+
const adjustedResetAt =
|
|
151
|
+
resetAtEpoch <= now ? _shiftDateByOneDay(resetAt) : resetAt;
|
|
152
|
+
const adjustedEpoch =
|
|
153
|
+
resetAtEpoch <= now ? resetAtEpoch + 24 * 60 * 60 * 1000 : resetAtEpoch;
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
resetAt: adjustedResetAt,
|
|
157
|
+
resetAtEpoch: adjustedEpoch,
|
|
158
|
+
sourceText,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface GLMQuotaSignal {
|
|
163
|
+
/** ISO timestamp when the quota resets */
|
|
164
|
+
resetAt: string;
|
|
165
|
+
/** Epoch ms when the quota resets */
|
|
166
|
+
resetAtEpoch: number;
|
|
167
|
+
/** Raw error text for logs */
|
|
168
|
+
sourceText: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
95
171
|
// ─── Policy ─────────────────────────────────────────────────────────────────
|
|
96
172
|
|
|
97
173
|
export interface SurgePolicy {
|
|
@@ -163,18 +239,29 @@ export class SurgeScheduler {
|
|
|
163
239
|
}
|
|
164
240
|
}
|
|
165
241
|
|
|
166
|
-
// ─── Retry
|
|
242
|
+
// ─── Retry wrappers ─────────────────────────────────────────────────────────
|
|
167
243
|
|
|
168
244
|
export interface SurgeRetryOptions {
|
|
169
245
|
policy?: Partial<SurgePolicy>;
|
|
246
|
+
/**
|
|
247
|
+
* Pre-existing scheduler to use. When provided, the attempt counter persists
|
|
248
|
+
* across calls — enabling true exponential escalation across multiple surges
|
|
249
|
+
* within the same task (e.g. surge on 2nd loop.invoke, surge on 3rd loop.invoke).
|
|
250
|
+
* If omitted, a fresh scheduler is created (attempt counter resets per call).
|
|
251
|
+
*/
|
|
252
|
+
scheduler?: SurgeScheduler;
|
|
170
253
|
/** Called before each surge pause (logging / notifications) */
|
|
171
254
|
onSurge?: (info: {
|
|
172
255
|
attempt: number;
|
|
173
256
|
delayMs: number;
|
|
174
257
|
signal: SurgeSignal;
|
|
175
258
|
}) => void;
|
|
176
|
-
/** Called once when attempts are exhausted (
|
|
177
|
-
|
|
259
|
+
/** Called once when attempts are exhausted (no more retries in this scheduler).
|
|
260
|
+
* Can be async to allow sleeping. Return true to retry, false/undefined to throw.
|
|
261
|
+
*/
|
|
262
|
+
onExhausted?: (
|
|
263
|
+
signal: SurgeSignal,
|
|
264
|
+
) => boolean | void | Promise<boolean | void>;
|
|
178
265
|
/** Injectable sleep for tests. Default: real setTimeout */
|
|
179
266
|
sleep?: (ms: number) => Promise<void>;
|
|
180
267
|
}
|
|
@@ -182,15 +269,16 @@ export interface SurgeRetryOptions {
|
|
|
182
269
|
const realSleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
183
270
|
|
|
184
271
|
/**
|
|
185
|
-
* Run `fn`, retrying only on surge-classified failures
|
|
186
|
-
*
|
|
187
|
-
* last error is rethrown after `onExhausted`.
|
|
272
|
+
* Run `fn`, retrying only on surge-classified failures (MiniMax 529/overload)
|
|
273
|
+
* with exponential backoff. Non-surge errors rethrow immediately.
|
|
188
274
|
*/
|
|
189
275
|
export async function invokeWithSurgeRetry<T>(
|
|
190
276
|
fn: () => Promise<T>,
|
|
191
277
|
opts: SurgeRetryOptions = {},
|
|
192
278
|
): Promise<T> {
|
|
193
|
-
|
|
279
|
+
// Reuse provided scheduler to persist attempt counter across calls.
|
|
280
|
+
// A fresh scheduler is created only when none is provided.
|
|
281
|
+
const scheduler = opts.scheduler ?? new SurgeScheduler(opts.policy);
|
|
194
282
|
const sleep = opts.sleep ?? realSleep;
|
|
195
283
|
|
|
196
284
|
for (;;) {
|
|
@@ -202,7 +290,12 @@ export async function invokeWithSurgeRetry<T>(
|
|
|
202
290
|
|
|
203
291
|
const attempt = scheduler.nextAttempt();
|
|
204
292
|
if (attempt === null) {
|
|
205
|
-
|
|
293
|
+
// Scheduler exhausted — call onExhausted. If it returns true, do NOT throw
|
|
294
|
+
// (caller handled it, e.g. slept and will retry). If false/undefined, throw.
|
|
295
|
+
const handled = await opts.onExhausted?.(signal);
|
|
296
|
+
if (handled === true) {
|
|
297
|
+
continue; // retry
|
|
298
|
+
}
|
|
206
299
|
throw err;
|
|
207
300
|
}
|
|
208
301
|
|
|
@@ -215,3 +308,79 @@ export async function invokeWithSurgeRetry<T>(
|
|
|
215
308
|
}
|
|
216
309
|
}
|
|
217
310
|
}
|
|
311
|
+
|
|
312
|
+
// ─── GLM Quota Retry ────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
export interface GLMRetryOptions {
|
|
315
|
+
/** Called when GLM quota exhaustion is detected (for status-line countdown) */
|
|
316
|
+
onGLMQuota?: (signal: GLMQuotaSignal) => void;
|
|
317
|
+
/** Called after the countdown expires and the loop retries */
|
|
318
|
+
onGLMRetry?: (signal: GLMQuotaSignal) => void;
|
|
319
|
+
/** Called when the GLM quota error is non-retryable (e.g., no reset time) */
|
|
320
|
+
onGLMExhausted?: (signal: GLMQuotaSignal) => void;
|
|
321
|
+
/** Injectable sleep for tests. Default: real setTimeout */
|
|
322
|
+
sleep?: (ms: number) => Promise<void>;
|
|
323
|
+
/** How often to tick the countdown callback (default: 30s) */
|
|
324
|
+
tickMs?: number;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Run `fn`, retrying on GLM 1308 quota errors.
|
|
329
|
+
* After the first 1308, waits until the reset time before retrying.
|
|
330
|
+
* Subsequent 1308s within the same wait window are silently ignored.
|
|
331
|
+
* Non-GLM-quota errors rethrow immediately.
|
|
332
|
+
*/
|
|
333
|
+
export async function invokeWithGLMRetry<T>(
|
|
334
|
+
fn: () => Promise<T>,
|
|
335
|
+
opts: GLMRetryOptions = {},
|
|
336
|
+
): Promise<T> {
|
|
337
|
+
const sleep = opts.sleep ?? realSleep;
|
|
338
|
+
const tickMs = opts.tickMs ?? 30_000;
|
|
339
|
+
|
|
340
|
+
let waitingForReset = false;
|
|
341
|
+
|
|
342
|
+
while (true) {
|
|
343
|
+
try {
|
|
344
|
+
return await fn();
|
|
345
|
+
} catch (err) {
|
|
346
|
+
const signal = classifyGLMQuota(err);
|
|
347
|
+
|
|
348
|
+
if (!signal) throw err; // not a GLM quota error — fail fast
|
|
349
|
+
|
|
350
|
+
// If we are already waiting for a reset and get another 1308,
|
|
351
|
+
// ignore it — the existing wait is already in progress.
|
|
352
|
+
if (waitingForReset) {
|
|
353
|
+
// Just log and continue waiting (the sleep loop handles the timeout)
|
|
354
|
+
await sleep(tickMs);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const delayMs = Math.max(0, signal.resetAtEpoch - Date.now());
|
|
359
|
+
|
|
360
|
+
opts.onGLMQuota?.(signal);
|
|
361
|
+
waitingForReset = true;
|
|
362
|
+
|
|
363
|
+
if (delayMs <= 0) {
|
|
364
|
+
// Reset time already passed — retry immediately
|
|
365
|
+
waitingForReset = false;
|
|
366
|
+
opts.onGLMRetry?.(signal);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Wait in chunks so onGLMQuota fires periodically (drives countdown display)
|
|
371
|
+
const startMs = Date.now();
|
|
372
|
+
while (Date.now() - startMs < delayMs) {
|
|
373
|
+
await sleep(tickMs);
|
|
374
|
+
// Re-fire the callback to refresh the countdown display
|
|
375
|
+
opts.onGLMQuota?.({
|
|
376
|
+
...signal,
|
|
377
|
+
resetAtEpoch: signal.resetAtEpoch, // unchanged
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Countdown expired — retry
|
|
382
|
+
waitingForReset = false;
|
|
383
|
+
opts.onGLMRetry?.(signal);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoopWidget — harness progress display for the pi host status line.
|
|
3
|
+
*
|
|
4
|
+
* Architecture mirrors pi-lens's `clients/widget-state.ts` + `renderWidget()`:
|
|
5
|
+
* - State store: per-file step counts, iteration, phase
|
|
6
|
+
* - Render function: builds the `!NW ●ME` style summary line
|
|
7
|
+
* - Pi host integration: wired via ExtensionUIContext.setWidget() when available
|
|
8
|
+
*
|
|
9
|
+
* The widget is designed as a dependency injected into LoopDaemon/LoopRuntime,
|
|
10
|
+
* so it works in two modes:
|
|
11
|
+
* 1. Standalone (console): logs human-readable progress to stderr
|
|
12
|
+
* 2. Pi host: calls ui.setWidget("harness-loop", renderFn) for TUI display
|
|
13
|
+
*
|
|
14
|
+
* Wiki: wiki/multi-agent-langchain.md
|
|
15
|
+
*/
|
|
16
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
17
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
/** Valid theme color keys passed to `theme.fg()`.*/
|
|
19
|
+
type ColorKey = "dim" | "error" | "warning" | "success" | "accent";
|
|
20
|
+
export type LoopPhase = "idle" | "planning" | "writing" | "reviewing" | "fixing" | "approving" | "blocked" | "surge-pause" | "complete" | "error";
|
|
21
|
+
export interface LoopWidgetOptions {
|
|
22
|
+
/** Called with the rendered lines each time the widget updates. */
|
|
23
|
+
onRender?: (lines: string[]) => void;
|
|
24
|
+
}
|
|
25
|
+
export declare class LoopWidget {
|
|
26
|
+
private files;
|
|
27
|
+
private phase;
|
|
28
|
+
private iteration;
|
|
29
|
+
private maxIterations;
|
|
30
|
+
private stepLabel;
|
|
31
|
+
private errors;
|
|
32
|
+
private warnings;
|
|
33
|
+
private readonly onRender?;
|
|
34
|
+
constructor(options?: LoopWidgetOptions);
|
|
35
|
+
/** Called when the loop starts a new iteration. */
|
|
36
|
+
startIteration(iter: number, max: number): void;
|
|
37
|
+
/** Called when the loop transitions to a new phase. */
|
|
38
|
+
setPhase(phase: LoopPhase, label?: string): void;
|
|
39
|
+
/** Called when the coder writes to a file. */
|
|
40
|
+
recordWrite(filePath: string): void;
|
|
41
|
+
/** Called when the reviewer reports blockers for a file. */
|
|
42
|
+
recordBlockers(filePath: string, count: number): void;
|
|
43
|
+
/** Called when the reviewer passes a file (no blockers). */
|
|
44
|
+
recordReviewPass(filePath: string): void;
|
|
45
|
+
/** Called on surge-pause / quota-wait events. */
|
|
46
|
+
setSurgePause(resetAt?: Date): void;
|
|
47
|
+
/** Called on unrecoverable error. */
|
|
48
|
+
setError(message: string): void;
|
|
49
|
+
/** Called when the loop completes with a verdict. */
|
|
50
|
+
setComplete(verdict: "approved" | "rejected" | "max_iterations"): void;
|
|
51
|
+
/** Reset to idle state. */
|
|
52
|
+
reset(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Build widget lines for the pi host's `setWidget()` renderer.
|
|
55
|
+
*
|
|
56
|
+
* Returns 0-3 lines:
|
|
57
|
+
* Line 1: `harness [iter 2/5] ✍ coder !2W` — header + summary
|
|
58
|
+
* Line 2: ` surge pause — resets 19:17` — surge detail (when applicable)
|
|
59
|
+
* Line 3: ` ●surge.ts ✍2 🔍3 !1` — file rows (up to 5)
|
|
60
|
+
*/
|
|
61
|
+
renderWidget(width: number, theme: {
|
|
62
|
+
fg: (color: ColorKey, s: string) => string;
|
|
63
|
+
}): string[];
|
|
64
|
+
/** Console fallback (standalone mode). */
|
|
65
|
+
toConsoleString(): string;
|
|
66
|
+
/**
|
|
67
|
+
* Returns a pi-tui Component factory for use with `ui.setWidget("harness-loop", factory)`.
|
|
68
|
+
* The caller (pi host integration) owns the `setWidget` call — this factory is just
|
|
69
|
+
* the render function.
|
|
70
|
+
*
|
|
71
|
+
* Usage in pi host:
|
|
72
|
+
* ```
|
|
73
|
+
* setWidget("harness-loop", makeWidgetRenderer(widget, theme), { placement: "belowEditor" });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
/**
|
|
77
|
+
* Returns a factory suitable for `ui.setWidget("harness-loop", factory)` in the
|
|
78
|
+
* pi host. The factory's `render(width)` is called by the pi-tui render loop
|
|
79
|
+
* on every frame; `invalidate` is called by the harness to trigger a re-render.
|
|
80
|
+
*/
|
|
81
|
+
makeRenderer(): {
|
|
82
|
+
factory: (tui: TUI, theme: Theme) => {
|
|
83
|
+
render: (width: number) => string[];
|
|
84
|
+
invalidate: () => void;
|
|
85
|
+
dispose: () => void;
|
|
86
|
+
};
|
|
87
|
+
invalidate: () => void;
|
|
88
|
+
};
|
|
89
|
+
private getOrCreate;
|
|
90
|
+
private fileRecords;
|
|
91
|
+
private fileBasename;
|
|
92
|
+
private classifyFileTier;
|
|
93
|
+
private phaseLabel;
|
|
94
|
+
private phaseIcon;
|
|
95
|
+
private formatResetTime;
|
|
96
|
+
/**
|
|
97
|
+
* Truncate `line` to `width` visible columns.
|
|
98
|
+
* Uses `visibleWidth` (not raw char count) so ANSI codes don't cause
|
|
99
|
+
* false wrapping — matching how the terminal measures columns.
|
|
100
|
+
*/
|
|
101
|
+
private fitLine;
|
|
102
|
+
private render;
|
|
103
|
+
}
|
|
104
|
+
export declare function getLoopWidget(): LoopWidget;
|
|
105
|
+
//# sourceMappingURL=widget.d.ts.map
|