pi-harness-runtime 1.1.394 → 1.1.396
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/AGENTS.md +135 -4
- package/harness/glm-quota-logger.ts +17 -0
- package/harness/langchain/planner-logger.ts +15 -0
- package/harness/service-diagnostics.ts +32 -1
- package/index.ts +92 -3
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/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/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/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/contracts/dist/workspace-env-format.d.ts +87 -0
- package/packages/contracts/dist/workspace-env-format.js +174 -6
- package/packages/contracts/dist/workspace-env.js +120 -9
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/src/workspace-env-format.ts +209 -6
- package/packages/contracts/src/workspace-env.ts +128 -7
- package/packages/contracts/test/workspace-env.test.ts +177 -0
- 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/durable-review/package.json +1 -1
- package/packages/e2e-tester/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-sink/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/jev-judge/dist/continuation-controller.d.ts +47 -0
- package/packages/jev-judge/dist/continuation-controller.d.ts.map +1 -1
- package/packages/jev-judge/dist/continuation-controller.js +142 -16
- package/packages/jev-judge/dist/continuation-controller.js.map +1 -1
- package/packages/jev-judge/dist/index.d.ts +6 -0
- package/packages/jev-judge/dist/index.d.ts.map +1 -1
- package/packages/jev-judge/dist/index.js +6 -0
- package/packages/jev-judge/dist/index.js.map +1 -1
- package/packages/jev-judge/dist/validation-ledger.d.ts +129 -0
- package/packages/jev-judge/dist/validation-ledger.d.ts.map +1 -0
- package/packages/jev-judge/dist/validation-ledger.js +234 -0
- package/packages/jev-judge/dist/validation-ledger.js.map +1 -0
- package/packages/jev-judge/dist/validation-plan.d.ts +176 -0
- package/packages/jev-judge/dist/validation-plan.d.ts.map +1 -0
- package/packages/jev-judge/dist/validation-plan.js +293 -0
- package/packages/jev-judge/dist/validation-plan.js.map +1 -0
- package/packages/jev-judge/dist/validation-tie-breaker.d.ts +48 -0
- package/packages/jev-judge/dist/validation-tie-breaker.d.ts.map +1 -0
- package/packages/jev-judge/dist/validation-tie-breaker.js +83 -0
- package/packages/jev-judge/dist/validation-tie-breaker.js.map +1 -0
- package/packages/jev-judge/package.json +1 -1
- package/packages/jev-judge/src/continuation-controller.ts +179 -20
- package/packages/jev-judge/src/index.ts +41 -0
- package/packages/jev-judge/src/validation-ledger.ts +290 -0
- package/packages/jev-judge/src/validation-plan.ts +406 -0
- package/packages/jev-judge/src/validation-tie-breaker.ts +123 -0
- package/packages/jev-judge/tests/continuation-controller.test.ts +167 -0
- package/packages/jev-judge/tests/validation-ledger.test.ts +181 -0
- package/packages/jev-judge/tests/validation-plan.test.ts +170 -0
- package/packages/jev-judge/tests/validation-tie-breaker.test.ts +88 -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/logging/README.md +37 -0
- package/packages/logging/dist/buffer.d.ts +33 -0
- package/packages/logging/dist/buffer.d.ts.map +1 -0
- package/packages/logging/dist/buffer.js +60 -0
- package/packages/logging/dist/buffer.js.map +1 -0
- package/packages/logging/dist/config.d.ts +40 -0
- package/packages/logging/dist/config.d.ts.map +1 -0
- package/packages/logging/dist/config.js +114 -0
- package/packages/logging/dist/config.js.map +1 -0
- package/packages/logging/dist/index.d.ts +32 -0
- package/packages/logging/dist/index.d.ts.map +1 -0
- package/packages/logging/dist/index.js +31 -0
- package/packages/logging/dist/index.js.map +1 -0
- package/packages/logging/dist/redact.d.ts +43 -0
- package/packages/logging/dist/redact.d.ts.map +1 -0
- package/packages/logging/dist/redact.js +220 -0
- package/packages/logging/dist/redact.js.map +1 -0
- package/packages/logging/dist/sampler.d.ts +37 -0
- package/packages/logging/dist/sampler.d.ts.map +1 -0
- package/packages/logging/dist/sampler.js +78 -0
- package/packages/logging/dist/sampler.js.map +1 -0
- package/packages/logging/dist/shipper.d.ts +82 -0
- package/packages/logging/dist/shipper.d.ts.map +1 -0
- package/packages/logging/dist/shipper.js +238 -0
- package/packages/logging/dist/shipper.js.map +1 -0
- package/packages/logging/dist/singleton.d.ts +38 -0
- package/packages/logging/dist/singleton.d.ts.map +1 -0
- package/packages/logging/dist/singleton.js +95 -0
- package/packages/logging/dist/singleton.js.map +1 -0
- package/packages/logging/dist/spool.d.ts +39 -0
- package/packages/logging/dist/spool.d.ts.map +1 -0
- package/packages/logging/dist/spool.js +110 -0
- package/packages/logging/dist/spool.js.map +1 -0
- package/packages/logging/dist/transports/r2.d.ts +31 -0
- package/packages/logging/dist/transports/r2.d.ts.map +1 -0
- package/packages/logging/dist/transports/r2.js +60 -0
- package/packages/logging/dist/transports/r2.js.map +1 -0
- package/packages/logging/dist/transports/webhook.d.ts +22 -0
- package/packages/logging/dist/transports/webhook.d.ts.map +1 -0
- package/packages/logging/dist/transports/webhook.js +37 -0
- package/packages/logging/dist/transports/webhook.js.map +1 -0
- package/packages/logging/dist/types.d.ts +73 -0
- package/packages/logging/dist/types.d.ts.map +1 -0
- package/packages/logging/dist/types.js +19 -0
- package/packages/logging/dist/types.js.map +1 -0
- package/packages/logging/package.json +37 -0
- package/packages/logging/src/buffer.ts +77 -0
- package/packages/logging/src/config.ts +163 -0
- package/packages/logging/src/index.ts +87 -0
- package/packages/logging/src/redact.ts +271 -0
- package/packages/logging/src/sampler.ts +97 -0
- package/packages/logging/src/shipper.ts +281 -0
- package/packages/logging/src/singleton.ts +142 -0
- package/packages/logging/src/spool.ts +139 -0
- package/packages/logging/src/transports/r2.ts +86 -0
- package/packages/logging/src/transports/webhook.ts +54 -0
- package/packages/logging/src/types.ts +85 -0
- package/packages/logging/test/buffer.test.ts +44 -0
- package/packages/logging/test/config.test.ts +97 -0
- package/packages/logging/test/integration.test.ts +135 -0
- package/packages/logging/test/redact.test.ts +109 -0
- package/packages/logging/test/sampler.test.ts +57 -0
- package/packages/logging/test/shipper.test.ts +152 -0
- package/packages/logging/test/spool.test.ts +93 -0
- 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/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/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/qdrant-skills/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/relay/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/skills/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/telegram/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/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/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/scripts/unified-webhook-server.ts +102 -0
- package/scripts/validation-run.ts +191 -0
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { TodoItem, TodoContext, TodoProvider } from "./todo-provider.js";
|
|
2
2
|
import { createTodoProvider } from "./todo-provider.js";
|
|
3
|
+
import {
|
|
4
|
+
buildValidationSteerSuffix,
|
|
5
|
+
formatDuration,
|
|
6
|
+
parseValidationPlan,
|
|
7
|
+
validationBudgetStatus,
|
|
8
|
+
type ValidationBudgetStatus,
|
|
9
|
+
type ValidationState,
|
|
10
|
+
} from "./validation-plan.js";
|
|
11
|
+
import type { ValidationLedger } from "./validation-ledger.js";
|
|
3
12
|
|
|
4
13
|
/**
|
|
5
14
|
* Simple debug logger that writes to file, not stdout
|
|
@@ -88,8 +97,24 @@ export interface ContinuationConfig {
|
|
|
88
97
|
* A dispatch that never settles must not block every later continuation.
|
|
89
98
|
*/
|
|
90
99
|
inFlightTimeoutMs?: number;
|
|
100
|
+
/**
|
|
101
|
+
* Ledger of validation runs.
|
|
102
|
+
*
|
|
103
|
+
* When a ledger is wired, the controller knows which tier already ran and
|
|
104
|
+
* whether the final tier is still pending, and it can pause an item whose
|
|
105
|
+
* declared time budget is spent.
|
|
106
|
+
*/
|
|
107
|
+
validationLedger?: ValidationLedger;
|
|
91
108
|
}
|
|
92
109
|
|
|
110
|
+
/**
|
|
111
|
+
* What a dispatch wants to send.
|
|
112
|
+
*
|
|
113
|
+
* A dispatch may also abort: the message builder returns the reason and the
|
|
114
|
+
* dispatch is dropped before anything is put on the wire.
|
|
115
|
+
*/
|
|
116
|
+
type DispatchPayload = { send: string } | { abort: string };
|
|
117
|
+
|
|
93
118
|
/**
|
|
94
119
|
* Continuation state
|
|
95
120
|
*/
|
|
@@ -148,6 +173,8 @@ export interface ContinuationResult {
|
|
|
148
173
|
nextItemId?: number;
|
|
149
174
|
question?: string;
|
|
150
175
|
reason?: string;
|
|
176
|
+
/** Budget status of the item the decision was about. */
|
|
177
|
+
budget?: ValidationBudgetStatus;
|
|
151
178
|
}
|
|
152
179
|
|
|
153
180
|
/**
|
|
@@ -198,9 +225,11 @@ export class TodoContinuationController {
|
|
|
198
225
|
private _circuitBreakerCooldownMs = 5 * 60 * 1000; // How long the breaker stays open
|
|
199
226
|
private _inFlightTimeoutMs = INFLIGHT_TIMEOUT_MS; // Watchdog for a hanging dispatch
|
|
200
227
|
private _attemptsByItem = new Map<number, number>(); // Successful dispatches per item
|
|
228
|
+
private _itemStartedAt = new Map<number, number>(); // First continuation of an item (elapsed-time budget)
|
|
201
229
|
private _totalAttempts = 0; // Successful dispatches in this session (runaway guard)
|
|
202
230
|
private _maxTotalSteerAttempts = 9; // Session-wide cap on successful dispatches
|
|
203
231
|
private _disposed = false; // Set on shutdown: no dispatch and no timer may act anymore
|
|
232
|
+
private _validationLedger?: ValidationLedger; // Optional ledger of validation runs
|
|
204
233
|
|
|
205
234
|
/** True while a dispatch is on the wire */
|
|
206
235
|
private get inFlight(): boolean {
|
|
@@ -215,6 +244,7 @@ export class TodoContinuationController {
|
|
|
215
244
|
this._circuitBreakerCooldownMs = config?.circuitBreakerCooldownMs ?? 5 * 60 * 1000;
|
|
216
245
|
this._maxConsecutiveFailures = config?.maxConsecutiveFailures ?? 3;
|
|
217
246
|
this._inFlightTimeoutMs = config?.inFlightTimeoutMs ?? INFLIGHT_TIMEOUT_MS;
|
|
247
|
+
this._validationLedger = config?.validationLedger;
|
|
218
248
|
this.config = {
|
|
219
249
|
jevFallbackMinWaitMs: 5 * 60 * 1000, // 5 minutes
|
|
220
250
|
routineThreshold: 0.7,
|
|
@@ -237,6 +267,56 @@ export class TodoContinuationController {
|
|
|
237
267
|
return this._attemptsByItem.get(itemId) ?? 0;
|
|
238
268
|
}
|
|
239
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Elapsed work time on an item, measured from the first continuation.
|
|
272
|
+
*
|
|
273
|
+
* `undefined` means the item was never continued, so no elapsed time can
|
|
274
|
+
* be reported yet.
|
|
275
|
+
*/
|
|
276
|
+
private elapsedForItem(itemId: number): number | undefined {
|
|
277
|
+
const startedAt = this._itemStartedAt.get(itemId);
|
|
278
|
+
if (startedAt === undefined) return undefined;
|
|
279
|
+
return Date.now() - startedAt;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Remember when work on an item started (first continuation only).
|
|
284
|
+
*/
|
|
285
|
+
private markItemStarted(itemId: number): void {
|
|
286
|
+
if (!this._itemStartedAt.has(itemId)) this._itemStartedAt.set(itemId, Date.now());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The validation state of an item, read from the ledger.
|
|
291
|
+
*
|
|
292
|
+
* `undefined` when no ledger is wired: the caller then falls back to the
|
|
293
|
+
* declared plan alone.
|
|
294
|
+
*/
|
|
295
|
+
private validationStateFor(item: TodoItem | undefined): ValidationState | undefined {
|
|
296
|
+
if (!item || !this._validationLedger) return undefined;
|
|
297
|
+
return this._validationLedger.stateFor(item);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The budget status of an item.
|
|
302
|
+
*
|
|
303
|
+
* Only a declared `budgetMinutes` arms a budget - the harness never
|
|
304
|
+
* invents one, because an invented budget could pause legitimate work.
|
|
305
|
+
*/
|
|
306
|
+
private budgetStatusFor(item: TodoItem | undefined): ValidationBudgetStatus {
|
|
307
|
+
const plan = parseValidationPlan(item);
|
|
308
|
+
return validationBudgetStatus(plan, item ? this.elapsedForItem(item.id) : undefined);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The reason reported when an item ran out of its declared time budget.
|
|
313
|
+
*/
|
|
314
|
+
private budgetAbortReason(item: TodoItem, budget: ValidationBudgetStatus): string {
|
|
315
|
+
const spent = formatDuration(budget.elapsedMs ?? 0);
|
|
316
|
+
const total = formatDuration(budget.budgetMs ?? 0);
|
|
317
|
+
return `budget exhausted for task ${item.id} (${spent} of ${total}) - pausing at a safe boundary`;
|
|
318
|
+
}
|
|
319
|
+
|
|
240
320
|
/**
|
|
241
321
|
* True while the circuit breaker forbids a send.
|
|
242
322
|
*
|
|
@@ -469,7 +549,18 @@ export class TodoContinuationController {
|
|
|
469
549
|
if (this._totalAttempts >= this._maxTotalSteerAttempts) {
|
|
470
550
|
return { action: "stop", reason: `max total attempts (${this._maxTotalSteerAttempts}) reached` };
|
|
471
551
|
}
|
|
472
|
-
|
|
552
|
+
|
|
553
|
+
// A declared time budget that is spent pauses the item at a safe
|
|
554
|
+
// boundary: the work stays open, the report says what is pending.
|
|
555
|
+
const budget = this.budgetStatusFor(item);
|
|
556
|
+
if (budget.exhausted) {
|
|
557
|
+
return {
|
|
558
|
+
action: "stop",
|
|
559
|
+
reason: this.budgetAbortReason(item, budget),
|
|
560
|
+
budget,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
473
564
|
this.state.currentItemId = item.id;
|
|
474
565
|
this.state.nextItemId = item.id;
|
|
475
566
|
return { action: "continue", nextItemId: item.id };
|
|
@@ -492,7 +583,18 @@ export class TodoContinuationController {
|
|
|
492
583
|
if (this._totalAttempts >= this._maxTotalSteerAttempts) {
|
|
493
584
|
return { action: "stop", reason: `max total attempts (${this._maxTotalSteerAttempts}) reached` };
|
|
494
585
|
}
|
|
495
|
-
|
|
586
|
+
|
|
587
|
+
// A declared time budget that is spent pauses the item at a safe
|
|
588
|
+
// boundary: the work stays open, the report says what is pending.
|
|
589
|
+
const budget = this.budgetStatusFor(item);
|
|
590
|
+
if (budget.exhausted) {
|
|
591
|
+
return {
|
|
592
|
+
action: "stop",
|
|
593
|
+
reason: this.budgetAbortReason(item, budget),
|
|
594
|
+
budget,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
496
598
|
this.state.currentItemId = item.id;
|
|
497
599
|
this.state.nextItemId = item.id;
|
|
498
600
|
return { action: "continue", nextItemId: item.id };
|
|
@@ -530,20 +632,41 @@ export class TodoContinuationController {
|
|
|
530
632
|
return;
|
|
531
633
|
}
|
|
532
634
|
|
|
533
|
-
//
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
: `Continue task: ${item.subject}`;
|
|
537
|
-
// A retry after a stalled dispatch gets the same duplicate warning as
|
|
538
|
-
// the automatic continuation path.
|
|
539
|
-
const finalMessage =
|
|
540
|
-
this._stalledItemId === item.id
|
|
541
|
-
? `${message} (retry after a stalled dispatch - a duplicate continuation for this item may still arrive)`
|
|
542
|
-
: message;
|
|
635
|
+
// The message is built when the dispatch is about to be sent: the
|
|
636
|
+
// validation context must reflect the state at that moment.
|
|
637
|
+
this.markItemStarted(item.id);
|
|
543
638
|
|
|
544
639
|
debugLog("TodoContinuation", "Sending continuation:", item.subject);
|
|
545
640
|
|
|
546
|
-
const result = await this.guardedDispatch(item.id,
|
|
641
|
+
const result = await this.guardedDispatch(item.id, (dispatched) => {
|
|
642
|
+
const target = dispatched ?? item;
|
|
643
|
+
const baseMessage = target.description
|
|
644
|
+
? `Continue task: ${target.subject}\n\n${target.description}`
|
|
645
|
+
: `Continue task: ${target.subject}`;
|
|
646
|
+
// A retry after a stalled dispatch gets the same duplicate warning
|
|
647
|
+
// as the automatic continuation path.
|
|
648
|
+
const retryNote =
|
|
649
|
+
this._stalledItemId === item.id
|
|
650
|
+
? " (retry after a stalled dispatch - a duplicate continuation for this item may still arrive)"
|
|
651
|
+
: "";
|
|
652
|
+
// A spent time budget pauses the item: nothing goes on the wire.
|
|
653
|
+
const budget = this.budgetStatusFor(target);
|
|
654
|
+
if (budget.exhausted) {
|
|
655
|
+
return { abort: this.budgetAbortReason(target, budget) };
|
|
656
|
+
}
|
|
657
|
+
// A declared staged-validation plan rides along with the
|
|
658
|
+
// continuation: the agent must know which tier is cheap before it
|
|
659
|
+
// picks a command.
|
|
660
|
+
const validation = buildValidationSteerSuffix(target, {
|
|
661
|
+
elapsedMs: this.elapsedForItem(target.id),
|
|
662
|
+
state: this.validationStateFor(target),
|
|
663
|
+
});
|
|
664
|
+
return {
|
|
665
|
+
send: validation
|
|
666
|
+
? `${baseMessage}${retryNote}\n\n${validation}`
|
|
667
|
+
: `${baseMessage}${retryNote}`,
|
|
668
|
+
};
|
|
669
|
+
});
|
|
547
670
|
if (result.dispatched) {
|
|
548
671
|
// Only a dispatch that actually reached the agent consumes an attempt
|
|
549
672
|
this.recordAttempt(item.id);
|
|
@@ -566,8 +689,15 @@ export class TodoContinuationController {
|
|
|
566
689
|
/**
|
|
567
690
|
* Guarded dispatch - checks all conditions before sending
|
|
568
691
|
* Uses in-flight guard to prevent race conditions
|
|
692
|
+
*
|
|
693
|
+
* The message is built by `buildMessage` at the moment the dispatch is
|
|
694
|
+
* about to send, so the payload always reflects the state at that moment
|
|
695
|
+
* (and the item comes from the snapshot read this dispatch already does).
|
|
569
696
|
*/
|
|
570
|
-
private async guardedDispatch(
|
|
697
|
+
private async guardedDispatch(
|
|
698
|
+
itemId: number,
|
|
699
|
+
buildMessage: (item: TodoItem | undefined) => DispatchPayload,
|
|
700
|
+
): Promise<GuardedDispatchResult> {
|
|
571
701
|
// 0. A disposed controller never dispatches.
|
|
572
702
|
if (this._disposed) {
|
|
573
703
|
debugLog("TodoContinuation", "Guarded dispatch: controller disposed, skipping");
|
|
@@ -716,6 +846,18 @@ export class TodoContinuationController {
|
|
|
716
846
|
this.scheduleGuardRelease();
|
|
717
847
|
|
|
718
848
|
try {
|
|
849
|
+
const payload = buildMessage(item);
|
|
850
|
+
if ("abort" in payload) {
|
|
851
|
+
// The message builder refused the dispatch (e.g. the item
|
|
852
|
+
// ran out of its declared time budget). Nothing goes on the
|
|
853
|
+
// wire, and the guard is released again.
|
|
854
|
+
debugLog("TodoContinuation", "Guarded dispatch: aborted before send:", payload.abort);
|
|
855
|
+
this.state.continuationQueued = false;
|
|
856
|
+
this.cancelGuardTimer();
|
|
857
|
+
this._blockedDispatches++;
|
|
858
|
+
return { dispatched: false, reason: payload.abort, blocked: true };
|
|
859
|
+
}
|
|
860
|
+
const message = payload.send;
|
|
719
861
|
await this.config.dispatch(message);
|
|
720
862
|
this.state.lastSteerMessage = message;
|
|
721
863
|
// Only the dispatch that owns the guard may reset the
|
|
@@ -824,14 +966,31 @@ export class TodoContinuationController {
|
|
|
824
966
|
};
|
|
825
967
|
}
|
|
826
968
|
|
|
827
|
-
|
|
828
|
-
this._stalledItemId === itemId
|
|
829
|
-
? `Continue todo item ID ${itemId}. (retry after a stalled dispatch - a duplicate continuation for this item may still arrive)`
|
|
830
|
-
: `Continue todo item ID ${itemId}.`;
|
|
969
|
+
this.markItemStarted(itemId);
|
|
831
970
|
debugLog("TodoContinuation", "Attempting steer to continue item", itemId);
|
|
832
971
|
|
|
833
|
-
//
|
|
834
|
-
|
|
972
|
+
// The message is built when the dispatch is about to be sent, from the
|
|
973
|
+
// item the dispatch itself read: one snapshot read, no extra latency,
|
|
974
|
+
// and the validation context is always current.
|
|
975
|
+
const result = await this.guardedDispatch(itemId, (item) => {
|
|
976
|
+
const baseMessage =
|
|
977
|
+
this._stalledItemId === itemId
|
|
978
|
+
? `Continue todo item ID ${itemId}. (retry after a stalled dispatch - a duplicate continuation for this item may still arrive)`
|
|
979
|
+
: `Continue todo item ID ${itemId}.`;
|
|
980
|
+
// A spent time budget pauses the item: nothing goes on the wire.
|
|
981
|
+
const budget = this.budgetStatusFor(item);
|
|
982
|
+
if (budget.exhausted && item) {
|
|
983
|
+
return { abort: this.budgetAbortReason(item, budget) };
|
|
984
|
+
}
|
|
985
|
+
// A declared staged-validation plan rides along with the
|
|
986
|
+
// continuation: the agent must know which tier is cheap before it
|
|
987
|
+
// picks a command.
|
|
988
|
+
const validation = buildValidationSteerSuffix(item, {
|
|
989
|
+
elapsedMs: this.elapsedForItem(itemId),
|
|
990
|
+
state: this.validationStateFor(item),
|
|
991
|
+
});
|
|
992
|
+
return { send: validation ? `${baseMessage}\n\n${validation}` : baseMessage };
|
|
993
|
+
});
|
|
835
994
|
if (result.dispatched) {
|
|
836
995
|
// Only a dispatch that actually reached the agent consumes an attempt
|
|
837
996
|
this.recordAttempt(itemId);
|
|
@@ -728,6 +728,47 @@ export type { ContinuationConfig, ContinuationState, ContinuationResult } from "
|
|
|
728
728
|
export { createTodoProvider } from "./todo-provider.js";
|
|
729
729
|
export type { TodoProvider, TodoAvailability, TodoItem, TodoStatus, TodoSnapshot, TodoContext } from "./todo-provider.js";
|
|
730
730
|
|
|
731
|
+
// Export staged validation plans
|
|
732
|
+
export {
|
|
733
|
+
parseValidationPlan,
|
|
734
|
+
formatValidationContext,
|
|
735
|
+
buildValidationSteerSuffix,
|
|
736
|
+
describeTier,
|
|
737
|
+
parseTier,
|
|
738
|
+
parseDurationMs,
|
|
739
|
+
formatDuration,
|
|
740
|
+
summarizeValidationState,
|
|
741
|
+
validationBudgetStatus,
|
|
742
|
+
} from "./validation-plan.js";
|
|
743
|
+
export type {
|
|
744
|
+
ValidationPlan,
|
|
745
|
+
ValidationTier,
|
|
746
|
+
ValidationTierKind,
|
|
747
|
+
ValidationState,
|
|
748
|
+
ValidationTierState,
|
|
749
|
+
ValidationBudgetStatus,
|
|
750
|
+
} from "./validation-plan.js";
|
|
751
|
+
|
|
752
|
+
// Export the validation ledger (durable record of the tiers that ran)
|
|
753
|
+
export {
|
|
754
|
+
ValidationLedger,
|
|
755
|
+
defaultLedgerPath,
|
|
756
|
+
parseRunRecord,
|
|
757
|
+
parseLedgerContent,
|
|
758
|
+
} from "./validation-ledger.js";
|
|
759
|
+
export type {
|
|
760
|
+
ValidationRunRecord,
|
|
761
|
+
ValidationRunStatus,
|
|
762
|
+
ValidationLedgerOptions,
|
|
763
|
+
} from "./validation-ledger.js";
|
|
764
|
+
|
|
765
|
+
// Export the validation tie-breaker (typed verdict about one item)
|
|
766
|
+
export {
|
|
767
|
+
tieBreakValidation,
|
|
768
|
+
formatTieBreaker,
|
|
769
|
+
} from "./validation-tie-breaker.js";
|
|
770
|
+
export type { ValidationVerdict, ValidationTieBreaker } from "./validation-tie-breaker.js";
|
|
771
|
+
|
|
731
772
|
// Export Task Intake
|
|
732
773
|
export { TaskIntakeController, createTaskIntake } from "./task-intake.js";
|
|
733
774
|
export type { TaskIntakeConfig, TaskIntakeResult } from "./task-intake.js";
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Ledger - a durable record of which validation tier ran, when,
|
|
3
|
+
* how long it took and whether it passed.
|
|
4
|
+
*
|
|
5
|
+
* The harness never runs the tests itself, so the ledger is written by the
|
|
6
|
+
* agent (or by any tool that knows the result) through
|
|
7
|
+
* `scripts/validation-run.ts`. The ledger is the only mechanical source for
|
|
8
|
+
* the question "did the final tier already pass?" - without it the harness
|
|
9
|
+
* would have to trust the agent's own report, and a todo could be completed
|
|
10
|
+
* while its final acceptance check is still pending.
|
|
11
|
+
*
|
|
12
|
+
* Storage is plain NDJSON (one JSON object per line) so the file stays
|
|
13
|
+
* inspectable with `tail`, `jq` or `less`:
|
|
14
|
+
*
|
|
15
|
+
* ```text
|
|
16
|
+
* ~/.pi-harness-runtime/logs/validation-runs.jsonl
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* The file is rotated when it grows past `maxBytes` (the previous content
|
|
20
|
+
* becomes `<path>.1`).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync } from "node:fs";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
26
|
+
import type { TodoItem } from "./todo-provider.js";
|
|
27
|
+
import {
|
|
28
|
+
parseValidationPlan,
|
|
29
|
+
summarizeValidationState,
|
|
30
|
+
type ValidationState,
|
|
31
|
+
type ValidationTierKind,
|
|
32
|
+
type ValidationTierState,
|
|
33
|
+
} from "./validation-plan.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Result of a single validation run.
|
|
37
|
+
*/
|
|
38
|
+
export type ValidationRunStatus = "passed" | "failed" | "timeout" | "error" | "skipped";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One recorded validation run.
|
|
42
|
+
*/
|
|
43
|
+
export interface ValidationRunRecord {
|
|
44
|
+
/** ISO timestamp of the end of the run. */
|
|
45
|
+
ts: string;
|
|
46
|
+
/** Which tier was executed. */
|
|
47
|
+
tier: ValidationTierKind;
|
|
48
|
+
/** Outcome of the run. */
|
|
49
|
+
status: ValidationRunStatus;
|
|
50
|
+
/** Todo item the run belongs to. */
|
|
51
|
+
itemId?: number;
|
|
52
|
+
/** Subject of the todo item (denormalized for readable logs). */
|
|
53
|
+
itemSubject?: string;
|
|
54
|
+
/** The command that was executed. */
|
|
55
|
+
command?: string;
|
|
56
|
+
/** Human label of the tier. */
|
|
57
|
+
label?: string;
|
|
58
|
+
/** Wall-clock duration of the run in milliseconds. */
|
|
59
|
+
durationMs?: number;
|
|
60
|
+
/** Number of records the tier exercised, when it is data-volume bound. */
|
|
61
|
+
datasetSize?: number;
|
|
62
|
+
/** Exit code of the command, when it produced one. */
|
|
63
|
+
exitCode?: number;
|
|
64
|
+
/** Free-form note (e.g. "5 of 320 tests failed"). */
|
|
65
|
+
note?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Aggregated validation state of a todo item.
|
|
70
|
+
*/
|
|
71
|
+
export type { ValidationState, ValidationTierState };
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Options for the ledger.
|
|
75
|
+
*/
|
|
76
|
+
export interface ValidationLedgerOptions {
|
|
77
|
+
/** Path of the NDJSON ledger file. */
|
|
78
|
+
path?: string;
|
|
79
|
+
/** Rotation threshold in bytes (default 5 MB). */
|
|
80
|
+
maxBytes?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Mirror sink: called for every recorded run.
|
|
83
|
+
*
|
|
84
|
+
* The harness uses it to ship the ledger through the central logging
|
|
85
|
+
* pipeline, so the runs are archived next to the other application logs.
|
|
86
|
+
*/
|
|
87
|
+
onRecord?: (record: ValidationRunRecord) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Default location of the ledger.
|
|
94
|
+
*/
|
|
95
|
+
export function defaultLedgerPath(): string {
|
|
96
|
+
return join(homedir(), ".pi-harness-runtime", "logs", "validation-runs.jsonl");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isRunStatus(value: unknown): value is ValidationRunStatus {
|
|
100
|
+
return (
|
|
101
|
+
value === "passed" ||
|
|
102
|
+
value === "failed" ||
|
|
103
|
+
value === "timeout" ||
|
|
104
|
+
value === "error" ||
|
|
105
|
+
value === "skipped"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Copy the optional fields of a raw ledger object onto a typed record.
|
|
111
|
+
*/
|
|
112
|
+
function copyOptionalFields(record: Record<string, unknown>, out: ValidationRunRecord): void {
|
|
113
|
+
if (typeof record.itemId === "number" && Number.isFinite(record.itemId)) out.itemId = record.itemId;
|
|
114
|
+
if (typeof record.itemSubject === "string") out.itemSubject = record.itemSubject;
|
|
115
|
+
if (typeof record.command === "string") out.command = record.command;
|
|
116
|
+
if (typeof record.label === "string") out.label = record.label;
|
|
117
|
+
if (typeof record.durationMs === "number" && Number.isFinite(record.durationMs)) {
|
|
118
|
+
out.durationMs = record.durationMs;
|
|
119
|
+
}
|
|
120
|
+
if (typeof record.datasetSize === "number" && Number.isFinite(record.datasetSize)) {
|
|
121
|
+
out.datasetSize = record.datasetSize;
|
|
122
|
+
}
|
|
123
|
+
if (typeof record.exitCode === "number" && Number.isFinite(record.exitCode)) {
|
|
124
|
+
out.exitCode = record.exitCode;
|
|
125
|
+
}
|
|
126
|
+
if (typeof record.note === "string") out.note = record.note;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parse one ledger line. Returns `null` for malformed lines.
|
|
131
|
+
*/
|
|
132
|
+
export function parseRunRecord(line: string): ValidationRunRecord | null {
|
|
133
|
+
const text = line.trim();
|
|
134
|
+
if (!text) return null;
|
|
135
|
+
let raw: unknown;
|
|
136
|
+
try {
|
|
137
|
+
raw = JSON.parse(text);
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
142
|
+
const record = raw as Record<string, unknown>;
|
|
143
|
+
const tier = record.tier === "fast" || record.tier === "full" ? record.tier : undefined;
|
|
144
|
+
if (!tier) return null;
|
|
145
|
+
if (!isRunStatus(record.status)) return null;
|
|
146
|
+
|
|
147
|
+
const out: ValidationRunRecord = {
|
|
148
|
+
ts: typeof record.ts === "string" ? record.ts : new Date(0).toISOString(),
|
|
149
|
+
tier,
|
|
150
|
+
status: record.status,
|
|
151
|
+
};
|
|
152
|
+
copyOptionalFields(record, out);
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Parse a whole ledger file. Malformed lines are skipped, not fatal.
|
|
158
|
+
*/
|
|
159
|
+
export function parseLedgerContent(content: string): ValidationRunRecord[] {
|
|
160
|
+
const records: ValidationRunRecord[] = [];
|
|
161
|
+
for (const line of content.split("\n")) {
|
|
162
|
+
const record = parseRunRecord(line);
|
|
163
|
+
if (record) records.push(record);
|
|
164
|
+
}
|
|
165
|
+
return records;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Durable ledger of validation runs.
|
|
170
|
+
*/
|
|
171
|
+
export class ValidationLedger {
|
|
172
|
+
private readonly path: string;
|
|
173
|
+
private readonly maxBytes: number;
|
|
174
|
+
private readonly onRecord?: (record: ValidationRunRecord) => void;
|
|
175
|
+
|
|
176
|
+
constructor(options: ValidationLedgerOptions = {}) {
|
|
177
|
+
this.path = options.path ?? process.env.PI_VALIDATION_LEDGER ?? defaultLedgerPath();
|
|
178
|
+
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
179
|
+
this.onRecord = options.onRecord;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Path of the ledger file. */
|
|
183
|
+
get filePath(): string {
|
|
184
|
+
return this.path;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Record a validation run.
|
|
189
|
+
*
|
|
190
|
+
* Never throws: a broken ledger must not break the work it describes.
|
|
191
|
+
*/
|
|
192
|
+
record(
|
|
193
|
+
input: Omit<ValidationRunRecord, "ts"> & { ts?: string },
|
|
194
|
+
): ValidationRunRecord | null {
|
|
195
|
+
const record: ValidationRunRecord = {
|
|
196
|
+
ts: input.ts ?? new Date().toISOString(),
|
|
197
|
+
tier: input.tier,
|
|
198
|
+
status: input.status,
|
|
199
|
+
};
|
|
200
|
+
if (input.itemId !== undefined) record.itemId = input.itemId;
|
|
201
|
+
if (input.itemSubject !== undefined) record.itemSubject = input.itemSubject;
|
|
202
|
+
if (input.command !== undefined) record.command = input.command;
|
|
203
|
+
if (input.label !== undefined) record.label = input.label;
|
|
204
|
+
if (input.durationMs !== undefined) record.durationMs = input.durationMs;
|
|
205
|
+
if (input.datasetSize !== undefined) record.datasetSize = input.datasetSize;
|
|
206
|
+
if (input.exitCode !== undefined) record.exitCode = input.exitCode;
|
|
207
|
+
if (input.note !== undefined) record.note = input.note;
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
this.append(record);
|
|
211
|
+
} catch {
|
|
212
|
+
// A ledger write failure is not a reason to fail the run it describes.
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
this.onRecord?.(record);
|
|
216
|
+
} catch {
|
|
217
|
+
// The mirror sink must never break the producer either.
|
|
218
|
+
}
|
|
219
|
+
return record;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Read every recorded run.
|
|
224
|
+
*/
|
|
225
|
+
read(): ValidationRunRecord[] {
|
|
226
|
+
try {
|
|
227
|
+
if (!existsSync(this.path)) return [];
|
|
228
|
+
return parseLedgerContent(readFileSync(this.path, "utf-8"));
|
|
229
|
+
} catch {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Every run recorded for one todo item.
|
|
236
|
+
*/
|
|
237
|
+
runsFor(itemId: number): ValidationRunRecord[] {
|
|
238
|
+
return this.read().filter((run) => run.itemId === itemId);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The latest run of one tier for one todo item.
|
|
243
|
+
*/
|
|
244
|
+
latestFor(itemId: number, tier: ValidationTierKind): ValidationRunRecord | undefined {
|
|
245
|
+
const runs = this.runsFor(itemId).filter((run) => run.tier === tier);
|
|
246
|
+
if (runs.length === 0) return undefined;
|
|
247
|
+
// The ledger is append-only: with an equal timestamp the later run wins.
|
|
248
|
+
return runs.reduce((a, b) => (a.ts > b.ts ? a : b));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The aggregated validation state of a todo item.
|
|
253
|
+
*/
|
|
254
|
+
stateFor(item: TodoItem | null | undefined): ValidationState {
|
|
255
|
+
const plan = parseValidationPlan(item);
|
|
256
|
+
const runs = item?.id === undefined ? [] : this.runsFor(item.id);
|
|
257
|
+
return summarizeValidationState(plan, runs);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* True when the item must not be completed yet because its final tier is
|
|
262
|
+
* still pending.
|
|
263
|
+
*/
|
|
264
|
+
isFinalTierPending(item: TodoItem | null | undefined): boolean {
|
|
265
|
+
return this.stateFor(item).finalTierPending;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private append(record: ValidationRunRecord): void {
|
|
269
|
+
const dir = dirname(this.path);
|
|
270
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
271
|
+
this.rotateIfNeeded();
|
|
272
|
+
appendFileSync(this.path, `${JSON.stringify(record)}\n`, "utf-8");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private rotateIfNeeded(): void {
|
|
276
|
+
if (!existsSync(this.path)) return;
|
|
277
|
+
let size = 0;
|
|
278
|
+
try {
|
|
279
|
+
size = statSync(this.path).size;
|
|
280
|
+
} catch {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (size < this.maxBytes) return;
|
|
284
|
+
try {
|
|
285
|
+
renameSync(this.path, `${this.path}.1`);
|
|
286
|
+
} catch {
|
|
287
|
+
// Rotation is best effort: an unwritable directory must not lose runs.
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|