@peakflo/agent-harness 0.2.10-rc.1 → 0.2.10-rc.2
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/README.md +109 -13
- package/dist/adapters/acp-adapter.js +26 -38
- package/dist/adapters/claude-code-adapter.d.ts +4 -14
- package/dist/adapters/claude-code-adapter.js +86 -22
- package/dist/adapters/codex-adapter.js +17 -6
- package/dist/adapters/codex-app-server-adapter.js +9 -9
- package/dist/adapters/cursor-acp.d.ts +2 -3
- package/dist/adapters/cursor-acp.js +2 -3
- package/dist/adapters/pi-adapter.js +3 -0
- package/dist/adapters/vendor-environment.d.ts +12 -0
- package/dist/adapters/vendor-environment.js +186 -0
- package/dist/cli.js +55 -1
- package/dist/github-command-guards.js +5 -0
- package/dist/github-workspace.js +30 -3
- package/dist/harness.d.ts +4 -0
- package/dist/harness.js +32 -7
- package/dist/options.js +18 -2
- package/dist/runtime/permission-mode.d.ts +2 -3
- package/dist/runtime/permission-mode.js +2 -5
- package/dist/runtime/session-runner.d.ts +2 -0
- package/dist/runtime/session-runner.js +88 -15
- package/dist/service.d.ts +57 -0
- package/dist/service.js +535 -0
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
exports.SessionRunner = void 0;
|
|
24
|
+
exports.approvalTimeoutMs = approvalTimeoutMs;
|
|
24
25
|
exports.qualifiedModelId = qualifiedModelId;
|
|
25
26
|
exports.describeProviderError = describeProviderError;
|
|
26
27
|
exports.withoutGatewayForVendorBackend = withoutGatewayForVendorBackend;
|
|
@@ -47,6 +48,13 @@ const task_files_1 = require("./task-files");
|
|
|
47
48
|
const FINAL_STEER_ACK_WAIT_MS = 5_000;
|
|
48
49
|
/** How often the runner asks the adapter for new output when nothing pushes. */
|
|
49
50
|
const POLL_INTERVAL_MS = 2_000;
|
|
51
|
+
const DEFAULT_APPROVAL_TIMEOUT_MS = 30 * 60_000;
|
|
52
|
+
function approvalTimeoutMs(value) {
|
|
53
|
+
const minutes = Number(value);
|
|
54
|
+
return Number.isFinite(minutes) && minutes > 0
|
|
55
|
+
? Math.min(minutes * 60_000, 24 * 60 * 60_000)
|
|
56
|
+
: DEFAULT_APPROVAL_TIMEOUT_MS;
|
|
57
|
+
}
|
|
50
58
|
/**
|
|
51
59
|
* How long an idle status must persist before the turn is considered over.
|
|
52
60
|
*
|
|
@@ -576,6 +584,7 @@ class SessionRunner {
|
|
|
576
584
|
openApproval = null;
|
|
577
585
|
/** The transcript identity of an ordinary agent question, if one is open. */
|
|
578
586
|
openQuestionPartId = null;
|
|
587
|
+
openQuestionStartedAt = null;
|
|
579
588
|
/**
|
|
580
589
|
* Expiry of the model token THIS RUNNING BACKEND is holding, when the
|
|
581
590
|
* control plane reported one — see `LeaseResponse.modelTokenExpiresAt`.
|
|
@@ -1199,6 +1208,10 @@ class SessionRunner {
|
|
|
1199
1208
|
}
|
|
1200
1209
|
]);
|
|
1201
1210
|
}
|
|
1211
|
+
else if (drained.approvalTimedOut) {
|
|
1212
|
+
stopReason = 'approval_timeout';
|
|
1213
|
+
error = drained.approvalTimedOutMessage;
|
|
1214
|
+
}
|
|
1202
1215
|
else if (drained.timedOut) {
|
|
1203
1216
|
// THE LAST-LINE CEILING, and only reached when an operator has
|
|
1204
1217
|
// actually configured `CODING_AGENT_MAX_TURN_MINUTES` — the default
|
|
@@ -1485,6 +1498,8 @@ class SessionRunner {
|
|
|
1485
1498
|
let timedOut = false;
|
|
1486
1499
|
let idleTimedOut = false;
|
|
1487
1500
|
let idleTimedOutMessage;
|
|
1501
|
+
let approvalTimedOut = false;
|
|
1502
|
+
let approvalTimedOutMessage;
|
|
1488
1503
|
const seenMessageIds = new Set();
|
|
1489
1504
|
const seenPartIds = new Set(alreadySeenPartIds);
|
|
1490
1505
|
const partContentLengths = new Map();
|
|
@@ -1529,6 +1544,61 @@ class SessionRunner {
|
|
|
1529
1544
|
try {
|
|
1530
1545
|
while (!signal.aborted) {
|
|
1531
1546
|
const now = Date.now();
|
|
1547
|
+
const approvalLimitMs = approvalTimeoutMs(process.env.CODING_AGENT_APPROVAL_TIMEOUT_MINUTES);
|
|
1548
|
+
if (this.openApproval &&
|
|
1549
|
+
this.approvalWaitStartedAt !== null &&
|
|
1550
|
+
now - this.approvalWaitStartedAt >= approvalLimitMs) {
|
|
1551
|
+
const open = this.openApproval;
|
|
1552
|
+
const capable = adapter;
|
|
1553
|
+
// Close first so a late answer cannot approve the timed-out action.
|
|
1554
|
+
this.closeApproval(session.sessionId);
|
|
1555
|
+
let denialTimer;
|
|
1556
|
+
try {
|
|
1557
|
+
if (capable.respondToApproval) {
|
|
1558
|
+
await Promise.race([
|
|
1559
|
+
capable.respondToApproval(backendSessionId, false),
|
|
1560
|
+
new Promise((resolveTimeout) => {
|
|
1561
|
+
denialTimer = setTimeout(resolveTimeout, 5_000);
|
|
1562
|
+
})
|
|
1563
|
+
]);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
catch (approvalError) {
|
|
1567
|
+
this.logger.warn(`[SessionRunner] approval timeout denial failed for ${session.sessionId}: ${String(approvalError)}`);
|
|
1568
|
+
}
|
|
1569
|
+
finally {
|
|
1570
|
+
if (denialTimer)
|
|
1571
|
+
clearTimeout(denialTimer);
|
|
1572
|
+
}
|
|
1573
|
+
approvalTimedOut = true;
|
|
1574
|
+
approvalTimedOutMessage = `Approval denied after ${formatMinutes(approvalLimitMs)} without an answer. Turn stopped.`;
|
|
1575
|
+
this.record(session.sessionId, [
|
|
1576
|
+
{
|
|
1577
|
+
id: `answer-${open.toolCallId}`,
|
|
1578
|
+
role: 'system',
|
|
1579
|
+
partType: 'text',
|
|
1580
|
+
content: `Denied: ${approvalTimedOutMessage}`
|
|
1581
|
+
}
|
|
1582
|
+
]);
|
|
1583
|
+
break;
|
|
1584
|
+
}
|
|
1585
|
+
if (this.openQuestionPartId &&
|
|
1586
|
+
this.openQuestionStartedAt !== null &&
|
|
1587
|
+
now - this.openQuestionStartedAt >= approvalLimitMs) {
|
|
1588
|
+
const questionId = this.openQuestionPartId;
|
|
1589
|
+
this.closeQuestion(session.sessionId);
|
|
1590
|
+
approvalTimedOut = true;
|
|
1591
|
+
approvalTimedOutMessage = `Question closed after ${formatMinutes(approvalLimitMs)} without an answer. Turn stopped.`;
|
|
1592
|
+
this.record(session.sessionId, [
|
|
1593
|
+
{
|
|
1594
|
+
id: `answer-${questionId.replace(/^question-/, '')}`,
|
|
1595
|
+
role: 'system',
|
|
1596
|
+
partType: 'text',
|
|
1597
|
+
content: approvalTimedOutMessage
|
|
1598
|
+
}
|
|
1599
|
+
]);
|
|
1600
|
+
break;
|
|
1601
|
+
}
|
|
1532
1602
|
// Historical approval wait PLUS whatever approval is open right now.
|
|
1533
1603
|
// Added to both the hard deadline and, below, the idle window — a
|
|
1534
1604
|
// turn stopped to ask a person something is blocked on them, not
|
|
@@ -1644,7 +1714,8 @@ class SessionRunner {
|
|
|
1644
1714
|
// Last one wins: a retry that also fails should report its own reason.
|
|
1645
1715
|
providerError = status.message;
|
|
1646
1716
|
}
|
|
1647
|
-
const quiet = status.type === 'idle' || status.type === 'error'
|
|
1717
|
+
const quiet = (status.type === 'idle' || status.type === 'error') &&
|
|
1718
|
+
!this.isAwaitingApproval();
|
|
1648
1719
|
idleSeen = quiet ? idleSeen + 1 : 0;
|
|
1649
1720
|
if (idleSeen >= IDLE_CONFIRMATIONS) {
|
|
1650
1721
|
break;
|
|
@@ -1667,20 +1738,16 @@ class SessionRunner {
|
|
|
1667
1738
|
* and the turn cut as `no_progress` in the same tick, before the
|
|
1668
1739
|
* person it was raised for ever sees it.
|
|
1669
1740
|
*
|
|
1670
|
-
*
|
|
1671
|
-
*
|
|
1672
|
-
* design, since a slow human must never read as a hung backend, but
|
|
1673
|
-
* that means an approval nobody ever answers can hold a turn open
|
|
1674
|
-
* indefinitely today. Pre-existing behaviour (unrelated to the LONG
|
|
1675
|
-
* TURNS fix), not changed here; a bounded approval wait — after
|
|
1676
|
-
* which the turn ends on its own rather than waiting forever — is a
|
|
1677
|
-
* real follow-up, not implemented in this PR.
|
|
1741
|
+
* Approval time is excluded from the idle and hard limits. The
|
|
1742
|
+
* separate approval limit above ends an unanswered request.
|
|
1678
1743
|
*/
|
|
1679
1744
|
const approvalWaitSinceProgress = this.approvalWaitMs -
|
|
1680
1745
|
approvalWaitAtLastProgress +
|
|
1681
1746
|
this.liveApprovalWaitMs();
|
|
1682
1747
|
const idleElapsed = Date.now() - lastProgressAt - approvalWaitSinceProgress;
|
|
1683
|
-
if (this.approvalWaitStartedAt === null &&
|
|
1748
|
+
if (this.approvalWaitStartedAt === null &&
|
|
1749
|
+
this.openQuestionPartId === null &&
|
|
1750
|
+
idleElapsed > idleLimitMs) {
|
|
1684
1751
|
this.logger.warn(`[SessionRunner] session ${session.sessionId} had no progress for ` +
|
|
1685
1752
|
`${Math.round(idleElapsed / 1000)}s (limit ${Math.round(idleLimitMs / 1000)}s)`);
|
|
1686
1753
|
idleTimedOut = true;
|
|
@@ -1720,7 +1787,7 @@ class SessionRunner {
|
|
|
1720
1787
|
await this.flush(session.sessionId, this.pending.splice(0), undefined, false);
|
|
1721
1788
|
}
|
|
1722
1789
|
}
|
|
1723
|
-
if (signal.aborted || timedOut || idleTimedOut) {
|
|
1790
|
+
if (signal.aborted || timedOut || idleTimedOut || approvalTimedOut) {
|
|
1724
1791
|
try {
|
|
1725
1792
|
await adapter.abortPrompt(backendSessionId, config);
|
|
1726
1793
|
}
|
|
@@ -1733,7 +1800,9 @@ class SessionRunner {
|
|
|
1733
1800
|
providerError,
|
|
1734
1801
|
timedOut,
|
|
1735
1802
|
idleTimedOut,
|
|
1736
|
-
idleTimedOutMessage
|
|
1803
|
+
idleTimedOutMessage,
|
|
1804
|
+
approvalTimedOut,
|
|
1805
|
+
approvalTimedOutMessage
|
|
1737
1806
|
};
|
|
1738
1807
|
}
|
|
1739
1808
|
/**
|
|
@@ -1783,9 +1852,8 @@ class SessionRunner {
|
|
|
1783
1852
|
/* ---------------------------------------------------------------- */
|
|
1784
1853
|
/** Wall-clock spent in the approval we are in RIGHT NOW, if any. */
|
|
1785
1854
|
liveApprovalWaitMs() {
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
: Date.now() - this.approvalWaitStartedAt;
|
|
1855
|
+
const startedAt = this.approvalWaitStartedAt ?? this.openQuestionStartedAt;
|
|
1856
|
+
return startedAt === null ? 0 : Date.now() - startedAt;
|
|
1789
1857
|
}
|
|
1790
1858
|
/**
|
|
1791
1859
|
* WIRE 2. Turn a pending backend permission into a `question` transcript part.
|
|
@@ -1978,7 +2046,11 @@ class SessionRunner {
|
|
|
1978
2046
|
}
|
|
1979
2047
|
closeQuestion(sessionId) {
|
|
1980
2048
|
if (this.openQuestionPartId) {
|
|
2049
|
+
if (this.openQuestionStartedAt !== null) {
|
|
2050
|
+
this.approvalWaitMs += Date.now() - this.openQuestionStartedAt;
|
|
2051
|
+
}
|
|
1981
2052
|
this.openQuestionPartId = null;
|
|
2053
|
+
this.openQuestionStartedAt = null;
|
|
1982
2054
|
this.deps.onApprovalStateChange?.(sessionId, false);
|
|
1983
2055
|
}
|
|
1984
2056
|
}
|
|
@@ -2158,6 +2230,7 @@ class SessionRunner {
|
|
|
2158
2230
|
part.id !== this.openApproval?.partId &&
|
|
2159
2231
|
this.openQuestionPartId !== part.id) {
|
|
2160
2232
|
this.openQuestionPartId = part.id;
|
|
2233
|
+
this.openQuestionStartedAt = Date.now();
|
|
2161
2234
|
this.deps.onApprovalStateChange?.(sessionId, true);
|
|
2162
2235
|
}
|
|
2163
2236
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
type Platform = NodeJS.Platform;
|
|
2
|
+
type Run = (file: string, args: string[]) => string;
|
|
3
|
+
interface ServiceConfig {
|
|
4
|
+
startArgs: string[];
|
|
5
|
+
nodePath: string;
|
|
6
|
+
cliPath: string;
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ServiceContext {
|
|
10
|
+
nodePath: string;
|
|
11
|
+
cliPath: string;
|
|
12
|
+
env: NodeJS.ProcessEnv;
|
|
13
|
+
home?: string;
|
|
14
|
+
platform?: Platform;
|
|
15
|
+
uid?: number;
|
|
16
|
+
run?: Run;
|
|
17
|
+
output?: (message: string) => void;
|
|
18
|
+
sleep?: (ms: number) => void;
|
|
19
|
+
portOccupied?: (port: number) => Promise<boolean>;
|
|
20
|
+
}
|
|
21
|
+
export declare function permanentServiceExitCode(platform: Platform, linuxCode: 2 | 78): number;
|
|
22
|
+
/** Only failures that a restart cannot repair belong in this list. */
|
|
23
|
+
export declare function permanentStartupFailure(error: unknown): string | null;
|
|
24
|
+
/** Existing service files use --stored-credential; new ones also use --service. */
|
|
25
|
+
export declare function isServiceStart(argv: string[]): boolean;
|
|
26
|
+
/** Parse only documented start options. A token can never enter service state. */
|
|
27
|
+
export declare function serviceStartArgs(input: string[], env: NodeJS.ProcessEnv, home: string): string[];
|
|
28
|
+
export declare function renderLaunchAgent(config: ServiceConfig, logDir: string): string;
|
|
29
|
+
export declare function renderSystemdUnit(config: ServiceConfig): string;
|
|
30
|
+
export declare function recordServiceFailure(reason: string, home?: string): void;
|
|
31
|
+
export declare function clearServiceFailure(home?: string): void;
|
|
32
|
+
export declare class ServiceManager {
|
|
33
|
+
private readonly context;
|
|
34
|
+
private readonly home;
|
|
35
|
+
private readonly platform;
|
|
36
|
+
private readonly run;
|
|
37
|
+
private readonly output;
|
|
38
|
+
private readonly stateFile;
|
|
39
|
+
private readonly serviceFile;
|
|
40
|
+
private readonly logDir;
|
|
41
|
+
constructor(context: ServiceContext);
|
|
42
|
+
private supported;
|
|
43
|
+
private readConfig;
|
|
44
|
+
private checkCredential;
|
|
45
|
+
private writeAndStart;
|
|
46
|
+
install(input: string[]): Promise<void>;
|
|
47
|
+
private checkNodeVersion;
|
|
48
|
+
repair(): void;
|
|
49
|
+
assertNoDuplicateRunner(options: {
|
|
50
|
+
workspaceRoot: string;
|
|
51
|
+
healthPort: number;
|
|
52
|
+
}): void;
|
|
53
|
+
status(): void;
|
|
54
|
+
uninstall(): void;
|
|
55
|
+
}
|
|
56
|
+
export declare function runServiceCommand(args: string[], context: ServiceContext): Promise<void>;
|
|
57
|
+
export {};
|