pi-harness-runtime 1.1.34 → 1.1.37
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/harness/langchain/daemon.ts +220 -32
- package/package.json +1 -1
- 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/package.json +1 -1
- 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/dist/glm-usage.js +1 -1
- package/packages/providers/dist/glm-usage.js.map +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/providers/src/glm-usage.ts +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/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- 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
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
import { randomUUID } from "node:crypto";
|
|
30
30
|
import {
|
|
31
31
|
existsSync,
|
|
32
|
-
mkdirSync,
|
|
33
32
|
readdirSync,
|
|
34
33
|
readFileSync,
|
|
35
34
|
writeFileSync,
|
|
@@ -90,6 +89,8 @@ export interface DaemonConfig {
|
|
|
90
89
|
costCapUsd?: number;
|
|
91
90
|
/** Surge (529/overloaded) retry policy. Default: 3→6→12 min, max 5 pauses */
|
|
92
91
|
surgePolicy?: Partial<SurgePolicy>;
|
|
92
|
+
/** Kill a task if it runs longer than this (ms). Prevents permanent freezes. Default: no timeout */
|
|
93
|
+
taskTimeoutMs?: number;
|
|
93
94
|
/** Use persistent file-based checkpointer for crash-resume. Default: false (MemorySaver). */
|
|
94
95
|
checkpointer?: boolean | string;
|
|
95
96
|
/** @internal Test injection — overrides dryRun/real deps entirely */
|
|
@@ -105,6 +106,16 @@ export interface DaemonConfig {
|
|
|
105
106
|
ntfyTopic?: string;
|
|
106
107
|
webhookUrl?: string;
|
|
107
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* Path to a tunnel-start script (e.g. tailscale-tunnel.sh).
|
|
111
|
+
* If set, the daemon health-checks the tunnel before each task and
|
|
112
|
+
* auto-restarts it if the target port is unreachable.
|
|
113
|
+
*
|
|
114
|
+
* Set to "auto" to search for scripts/tailscale-tunnel.sh in the
|
|
115
|
+
* project root, or give an absolute path.
|
|
116
|
+
* Default: none (no tunnel management)
|
|
117
|
+
*/
|
|
118
|
+
tunnelCommand?: string;
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
// ─── TriggeredTask ──────────────────────────────────────────────────────────
|
|
@@ -199,6 +210,13 @@ function effectivePolicy(
|
|
|
199
210
|
return policy;
|
|
200
211
|
}
|
|
201
212
|
|
|
213
|
+
/** Rejects after `ms` milliseconds — used to time-box task execution. */
|
|
214
|
+
function timeoutPromise(ms: number): Promise<never> {
|
|
215
|
+
return new Promise<never>((_, reject) =>
|
|
216
|
+
setTimeout(() => reject(new Error(`Task timed out after ${ms}ms`)), ms),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
202
220
|
/** Wait (poll) for a human ack file or bus event. Returns true if approved. */
|
|
203
221
|
async function waitForAck(
|
|
204
222
|
task: TriggeredTask,
|
|
@@ -498,7 +516,135 @@ class BdTasksWatcher {
|
|
|
498
516
|
}
|
|
499
517
|
}
|
|
500
518
|
|
|
501
|
-
// ───
|
|
519
|
+
// ─── Tunnel Health Monitor ───────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Monitors the health of a SSH tunnel (e.g. tailscale-tunnel.sh) and
|
|
523
|
+
* auto-restarts it if the target port becomes unreachable.
|
|
524
|
+
*
|
|
525
|
+
* This prevents the daemon from freezing when the tunnel drops silently —
|
|
526
|
+
* a common cause of tasks that appear to hang forever.
|
|
527
|
+
*/
|
|
528
|
+
class TunnelMonitor {
|
|
529
|
+
private readonly scriptPath: string;
|
|
530
|
+
private readonly checkIntervalMs: number;
|
|
531
|
+
private readonly targetHost: string;
|
|
532
|
+
private readonly targetPort: number;
|
|
533
|
+
private running = false;
|
|
534
|
+
private timer?: ReturnType<typeof setTimeout>;
|
|
535
|
+
private consecutiveFailures = 0;
|
|
536
|
+
private readonly maxConsecutiveFailures = 2;
|
|
537
|
+
|
|
538
|
+
constructor(scriptPath: string, opts: {
|
|
539
|
+
checkIntervalMs?: number;
|
|
540
|
+
targetHost?: string;
|
|
541
|
+
targetPort?: number;
|
|
542
|
+
} = {}) {
|
|
543
|
+
this.scriptPath = scriptPath;
|
|
544
|
+
this.checkIntervalMs = opts.checkIntervalMs ?? 30_000; // 30s default
|
|
545
|
+
this.targetHost = opts.targetHost ?? "127.0.0.1";
|
|
546
|
+
this.targetPort = opts.targetPort ?? 5433;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
start(): void {
|
|
550
|
+
this.running = true;
|
|
551
|
+
console.log(
|
|
552
|
+
`[tunnel-monitor] Starting — script=${this.scriptPath}, check every ${this.checkIntervalMs / 1000}s, target=${this.targetHost}:${this.targetPort}`,
|
|
553
|
+
);
|
|
554
|
+
this._check();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
stop(): void {
|
|
558
|
+
this.running = false;
|
|
559
|
+
if (this.timer) {
|
|
560
|
+
clearTimeout(this.timer);
|
|
561
|
+
this.timer = undefined;
|
|
562
|
+
}
|
|
563
|
+
console.log("[tunnel-monitor] Stopped.");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Run a health check and restart if the port is down.
|
|
568
|
+
* Returns true if the tunnel is healthy.
|
|
569
|
+
*/
|
|
570
|
+
async checkAndFix(): Promise<boolean> {
|
|
571
|
+
const healthy = await this._isPortOpen();
|
|
572
|
+
if (healthy) {
|
|
573
|
+
this.consecutiveFailures = 0;
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
this.consecutiveFailures++;
|
|
578
|
+
console.warn(
|
|
579
|
+
`[tunnel-monitor] Port ${this.targetHost}:${this.targetPort} unreachable (failure #${this.consecutiveFailures})`,
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
|
|
583
|
+
console.warn(`[tunnel-monitor] Tunnel appears down — restarting...`);
|
|
584
|
+
await this._restartTunnel();
|
|
585
|
+
this.consecutiveFailures = 0;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private async _check(): Promise<void> {
|
|
592
|
+
if (!this.running) return;
|
|
593
|
+
await this.checkAndFix();
|
|
594
|
+
this.timer = setTimeout(() => this._check(), this.checkIntervalMs);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private async _isPortOpen(): Promise<boolean> {
|
|
598
|
+
const { createConnection } = await import("node:net");
|
|
599
|
+
return new Promise((resolve) => {
|
|
600
|
+
const socket = createConnection({
|
|
601
|
+
host: this.targetHost,
|
|
602
|
+
port: this.targetPort,
|
|
603
|
+
timeout: 2_000, // 2s timeout — just checking reachability
|
|
604
|
+
});
|
|
605
|
+
socket.on("connect", () => {
|
|
606
|
+
socket.destroy();
|
|
607
|
+
resolve(true);
|
|
608
|
+
});
|
|
609
|
+
socket.on("timeout", () => {
|
|
610
|
+
socket.destroy();
|
|
611
|
+
resolve(false);
|
|
612
|
+
});
|
|
613
|
+
socket.on("error", () => {
|
|
614
|
+
resolve(false);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private async _restartTunnel(): Promise<void> {
|
|
620
|
+
const { exec } = await import("node:child_process");
|
|
621
|
+
const { promisify } = await import("node:util");
|
|
622
|
+
const execAsync = promisify(exec);
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
// Stop first
|
|
626
|
+
await execAsync(`bash "${this.scriptPath}" stop 2>/dev/null`, {
|
|
627
|
+
timeout: 10_000,
|
|
628
|
+
}).catch(() => {}); // ignore errors on stop
|
|
629
|
+
|
|
630
|
+
// Small delay before restart
|
|
631
|
+
await new Promise((r) => setTimeout(r, 1_000));
|
|
632
|
+
|
|
633
|
+
// Start
|
|
634
|
+
const { stdout, stderr } = await execAsync(
|
|
635
|
+
`bash "${this.scriptPath}" start 2>&1`,
|
|
636
|
+
{ timeout: 30_000 },
|
|
637
|
+
);
|
|
638
|
+
console.log(`[tunnel-monitor] Restart output: ${stdout.trim() || stderr.trim() || "ok"}`);
|
|
639
|
+
} catch (err) {
|
|
640
|
+
console.error(
|
|
641
|
+
`[tunnel-monitor] Restart failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ─── Loop Daemon ───────────────────────────────────────────────────────────
|
|
502
648
|
|
|
503
649
|
/**
|
|
504
650
|
* The main daemon class. Coordinates watchers, leasing, loop execution, and
|
|
@@ -521,6 +667,7 @@ export class LoopDaemon {
|
|
|
521
667
|
private readonly inboxWatcher: InboxWatcher;
|
|
522
668
|
private readonly busWatcher: BusWatcher;
|
|
523
669
|
private readonly bdWatcher: BdTasksWatcher;
|
|
670
|
+
private readonly tunnelMonitor: TunnelMonitor | null;
|
|
524
671
|
|
|
525
672
|
/** Currently running tasks (taskId → RunningTask) */
|
|
526
673
|
private readonly running = new Map<string, RunningTask>();
|
|
@@ -537,10 +684,13 @@ export class LoopDaemon {
|
|
|
537
684
|
approvalPolicy: config.approvalPolicy ?? "never",
|
|
538
685
|
dryRun: config.dryRun ?? false,
|
|
539
686
|
costCapUsd: config.costCapUsd ?? undefined,
|
|
687
|
+
surgePolicy: config.surgePolicy,
|
|
688
|
+
taskTimeoutMs: config.taskTimeoutMs,
|
|
689
|
+
checkpointer: config.checkpointer,
|
|
540
690
|
sources: config.sources ?? ["inbox", "bus"],
|
|
541
691
|
workspace: config.workspace ?? getHerdrWorkspace(),
|
|
542
692
|
notificationConfig: config.notificationConfig ?? undefined,
|
|
543
|
-
|
|
693
|
+
tunnelCommand: config.tunnelCommand,
|
|
544
694
|
} as Required<DaemonConfig>;
|
|
545
695
|
|
|
546
696
|
this.agentId = this.config.agentId;
|
|
@@ -571,6 +721,13 @@ export class LoopDaemon {
|
|
|
571
721
|
this.busWatcher = new BusWatcher(this.bus, this.config.pollMs);
|
|
572
722
|
this.bdWatcher = new BdTasksWatcher(this.config.pollMs);
|
|
573
723
|
|
|
724
|
+
// Tunnel monitor — only active when a tunnel script is configured
|
|
725
|
+
if (this.config.tunnelCommand) {
|
|
726
|
+
this.tunnelMonitor = new TunnelMonitor(this.config.tunnelCommand);
|
|
727
|
+
} else {
|
|
728
|
+
this.tunnelMonitor = null;
|
|
729
|
+
}
|
|
730
|
+
|
|
574
731
|
// Reap stale leases from previous runs
|
|
575
732
|
const reclaimed = this.leaseManager.recoverOnStartup();
|
|
576
733
|
if (reclaimed.length > 0) {
|
|
@@ -604,6 +761,8 @@ export class LoopDaemon {
|
|
|
604
761
|
this.bdWatcher.start((t) => this._onTriggered(t));
|
|
605
762
|
}
|
|
606
763
|
|
|
764
|
+
this.tunnelMonitor?.start();
|
|
765
|
+
|
|
607
766
|
console.log("[daemon] Started.");
|
|
608
767
|
}
|
|
609
768
|
|
|
@@ -615,6 +774,7 @@ export class LoopDaemon {
|
|
|
615
774
|
this.inboxWatcher.stop();
|
|
616
775
|
this.busWatcher.stop();
|
|
617
776
|
this.bdWatcher.stop();
|
|
777
|
+
this.tunnelMonitor?.stop();
|
|
618
778
|
|
|
619
779
|
// Release all held leases
|
|
620
780
|
for (const [taskId, _runningTask] of this.running) {
|
|
@@ -679,6 +839,14 @@ export class LoopDaemon {
|
|
|
679
839
|
const log = (msg: string) => console.log(`[${loopId}] ${msg}`);
|
|
680
840
|
|
|
681
841
|
try {
|
|
842
|
+
// ── Tunnel health check — self-heal before running a task ───────────────
|
|
843
|
+
if (this.tunnelMonitor) {
|
|
844
|
+
const tunnelOk = await this.tunnelMonitor.checkAndFix();
|
|
845
|
+
if (!tunnelOk) {
|
|
846
|
+
log("Tunnel health check failed — waiting for tunnel restart...");
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
682
850
|
// ── Gate check (I7) ──────────────────────────────────────────────
|
|
683
851
|
const policy = effectivePolicy(task, this.config);
|
|
684
852
|
if (policy === "always" || policy === "on_blocked") {
|
|
@@ -722,39 +890,59 @@ export class LoopDaemon {
|
|
|
722
890
|
log(
|
|
723
891
|
`Starting write-review loop (maxIterations=${this.config.maxIterations})`,
|
|
724
892
|
);
|
|
725
|
-
const checkpointer =
|
|
726
|
-
this.config.checkpointer === false
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
893
|
+
const checkpointer = (() => {
|
|
894
|
+
if (this.config.checkpointer === false) return false;
|
|
895
|
+
if (this.config.checkpointer === true || typeof this.config.checkpointer === "string") {
|
|
896
|
+
return createLoopCheckpointer(
|
|
897
|
+
this.config.checkpointer === true
|
|
898
|
+
? this.bus.getWorkspace()
|
|
899
|
+
: (this.config.checkpointer as string),
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
return undefined; // default MemorySaver
|
|
903
|
+
})();
|
|
736
904
|
const loop: WriteReviewLoop = buildWriteReviewLoop(deps, { checkpointer });
|
|
737
905
|
|
|
738
|
-
const
|
|
739
|
-
(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
{
|
|
745
|
-
policy: this.config.surgePolicy,
|
|
746
|
-
onSurge: ({ attempt, delayMs }) =>
|
|
747
|
-
log(
|
|
748
|
-
`Provider surge (529) — resume in ${Math.round(delayMs / 1000)}s (attempt ${attempt})`,
|
|
906
|
+
const invokeTask = (signal?: AbortSignal) =>
|
|
907
|
+
invokeWithSurgeRetry(
|
|
908
|
+
() =>
|
|
909
|
+
loop.invoke(
|
|
910
|
+
{ request: task.request },
|
|
911
|
+
{ configurable: { thread_id: loopId }, signal },
|
|
749
912
|
),
|
|
750
|
-
|
|
751
|
-
this.
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
913
|
+
{
|
|
914
|
+
policy: this.config.surgePolicy,
|
|
915
|
+
onSurge: ({ attempt, delayMs }) =>
|
|
916
|
+
log(
|
|
917
|
+
`Provider surge (529) — resume in ${Math.round(delayMs / 1000)}s (attempt ${attempt})`,
|
|
918
|
+
),
|
|
919
|
+
onExhausted: () => {
|
|
920
|
+
this._notifyHumanReviewNeeded(
|
|
921
|
+
task,
|
|
922
|
+
"Provider surged past max surge attempts — task failed",
|
|
923
|
+
);
|
|
924
|
+
},
|
|
755
925
|
},
|
|
756
|
-
|
|
757
|
-
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
let finalState: Awaited<ReturnType<typeof invokeTask>>;
|
|
929
|
+
if (this.config.taskTimeoutMs) {
|
|
930
|
+
const ac = new AbortController();
|
|
931
|
+
const timeout = setTimeout(() => {
|
|
932
|
+
log(`Task timed out after ${this.config.taskTimeoutMs}ms — aborting`);
|
|
933
|
+
ac.abort();
|
|
934
|
+
}, this.config.taskTimeoutMs);
|
|
935
|
+
try {
|
|
936
|
+
finalState = await Promise.race([
|
|
937
|
+
invokeTask(ac.signal),
|
|
938
|
+
timeoutPromise(this.config.taskTimeoutMs),
|
|
939
|
+
]);
|
|
940
|
+
} finally {
|
|
941
|
+
clearTimeout(timeout);
|
|
942
|
+
}
|
|
943
|
+
} else {
|
|
944
|
+
finalState = await invokeTask();
|
|
945
|
+
}
|
|
758
946
|
|
|
759
947
|
const verdict = finalState.review?.verdict ?? "blocked";
|
|
760
948
|
const iterations = finalState.iteration ?? 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-harness/capability-registry",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.37",
|
|
4
4
|
"description": "Capability Registry for pi-harness-runtime — tracks model capabilities and enables capability-based routing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|