@impulselab/hepha 0.2.280 → 0.2.281
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 +4 -1
- package/dist/index.js +249 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/cli/SKILL.md +7 -6
package/README.md
CHANGED
|
@@ -37,7 +37,10 @@ hepha sessions list --json
|
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
`HEPHA_API_KEY` takes precedence over a stored login. `HEPHA_URL` can point the
|
|
40
|
-
CLI at another Hephaistos deployment.
|
|
40
|
+
CLI at another Hephaistos deployment. `HEPHA_TIMEOUT_MS` bounds one request;
|
|
41
|
+
`HEPHA_FOLLOW_MAX_OUTAGE_SECONDS` makes `--follow` give up that long after its
|
|
42
|
+
first failure, exiting with that failure — unset, it waits for the platform to
|
|
43
|
+
come back.
|
|
41
44
|
|
|
42
45
|
## Exit codes
|
|
43
46
|
|
package/dist/index.js
CHANGED
|
@@ -122,6 +122,11 @@ var CliOutput = class {
|
|
|
122
122
|
}
|
|
123
123
|
error(text) {
|
|
124
124
|
process.stderr.write(`${this.paint(`error: ${this.safe(text)}`, "red")}
|
|
125
|
+
`);
|
|
126
|
+
}
|
|
127
|
+
// Progress a long command owes the user; stderr keeps `--json` stdout whole.
|
|
128
|
+
warn(text) {
|
|
129
|
+
process.stderr.write(`${this.paint(this.safe(text), "yellow")}
|
|
125
130
|
`);
|
|
126
131
|
}
|
|
127
132
|
/**
|
|
@@ -387,6 +392,10 @@ ENVIRONMENT
|
|
|
387
392
|
HEPHA_TOKEN A device-flow session token, instead of ~/.hepha/config.json.
|
|
388
393
|
HEPHA_URL Platform base URL.
|
|
389
394
|
HEPHA_TIMEOUT_MS Per-request timeout in milliseconds (default 30000).
|
|
395
|
+
HEPHA_FOLLOW_MAX_OUTAGE_SECONDS
|
|
396
|
+
Give up this long after --follow's first failure, with that
|
|
397
|
+
failure as the exit code. Unset, it waits for the platform
|
|
398
|
+
to come back.
|
|
390
399
|
|
|
391
400
|
GLOBAL FLAGS
|
|
392
401
|
--json Machine-readable output on stdout. Every command that
|
|
@@ -632,7 +641,11 @@ function retryAfterSeconds(response) {
|
|
|
632
641
|
return void 0;
|
|
633
642
|
}
|
|
634
643
|
const seconds = Number(raw.trim());
|
|
635
|
-
|
|
644
|
+
if (Number.isFinite(seconds)) {
|
|
645
|
+
return seconds >= 0 ? seconds : void 0;
|
|
646
|
+
}
|
|
647
|
+
const date = Date.parse(raw.trim());
|
|
648
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, (date - Date.now()) / 1e3);
|
|
636
649
|
}
|
|
637
650
|
|
|
638
651
|
// src/lib/response/fetch-response.ts
|
|
@@ -841,6 +854,8 @@ function expectShape(payload, endpoint, requirements) {
|
|
|
841
854
|
// src/lib/is/is.ts
|
|
842
855
|
var is = {
|
|
843
856
|
string: (value) => typeof value === "string",
|
|
857
|
+
// Absent passes: a field a deployment older than this binary does not send.
|
|
858
|
+
optionalString: (value) => value === void 0 || typeof value === "string",
|
|
844
859
|
number: (value) => typeof value === "number",
|
|
845
860
|
array: (value) => Array.isArray(value),
|
|
846
861
|
object: (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value)
|
|
@@ -1180,7 +1195,7 @@ var deviceTokenError = {
|
|
|
1180
1195
|
}
|
|
1181
1196
|
};
|
|
1182
1197
|
|
|
1183
|
-
// src/
|
|
1198
|
+
// src/lib/retry/next-poll-delay.ts
|
|
1184
1199
|
function nextPollDelay(interval, remainingSeconds) {
|
|
1185
1200
|
if (remainingSeconds <= 0) {
|
|
1186
1201
|
return null;
|
|
@@ -1739,28 +1754,224 @@ function consumeSessionEvents(events, cursor, quiet) {
|
|
|
1739
1754
|
return cursor;
|
|
1740
1755
|
}
|
|
1741
1756
|
|
|
1742
|
-
// src/lib/
|
|
1743
|
-
|
|
1757
|
+
// src/lib/sleep/sleep.ts
|
|
1758
|
+
function sleep2(seconds) {
|
|
1759
|
+
return new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// src/lib/sleep/sleep-constants.ts
|
|
1763
|
+
var MAX_TIMER_MS = 2147483647;
|
|
1764
|
+
|
|
1765
|
+
// src/lib/sleep/timer-delay-ms.ts
|
|
1766
|
+
function timerDelayMs(seconds) {
|
|
1767
|
+
return Math.min(Math.max(0, seconds) * 1e3, MAX_TIMER_MS);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/lib/sleep/interruptible-sleep.ts
|
|
1771
|
+
function interruptibleSleep(seconds) {
|
|
1772
|
+
return new Promise((resolve, reject) => {
|
|
1773
|
+
const onInterrupt = () => {
|
|
1774
|
+
clearTimeout(timer);
|
|
1775
|
+
reject(
|
|
1776
|
+
new CliError("Follow interrupted.", 130, {
|
|
1777
|
+
code: CLI_ERROR_CODES.INTERRUPTED
|
|
1778
|
+
})
|
|
1779
|
+
);
|
|
1780
|
+
};
|
|
1781
|
+
const timer = setTimeout(() => {
|
|
1782
|
+
process.removeListener("SIGINT", onInterrupt);
|
|
1783
|
+
resolve();
|
|
1784
|
+
}, timerDelayMs(seconds));
|
|
1785
|
+
process.once("SIGINT", onInterrupt);
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// src/lib/session/default-follow-session-dependencies.ts
|
|
1790
|
+
var defaultFollowSessionDependencies = {
|
|
1791
|
+
request: api,
|
|
1792
|
+
pollSleep: sleep2,
|
|
1793
|
+
retrySleep: interruptibleSleep,
|
|
1794
|
+
random: Math.random,
|
|
1795
|
+
// Monotonic: an NTP step backwards must not hand the outage budget more time
|
|
1796
|
+
// to burn, nor mute the periodic reminder until the clock catches up.
|
|
1797
|
+
now: () => performance.now(),
|
|
1798
|
+
warn: (message) => out.warn(message)
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
// src/lib/session/retry/follow-outage-message.ts
|
|
1802
|
+
function followOutageMessage(warned) {
|
|
1803
|
+
return warned ? "Hepha API still unavailable; retrying follow." : "Hepha API unavailable; retrying follow.";
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// src/lib/session/retry/follow-retry-constants.ts
|
|
1807
|
+
var FOLLOW_RETRY_CAP_SECONDS = 22.5;
|
|
1808
|
+
var FOLLOW_OUTAGE_FEEDBACK_MS = 5 * 60 * 1e3;
|
|
1809
|
+
var RETRYABLE_FOLLOW_STATUS = /* @__PURE__ */ new Set([
|
|
1810
|
+
408,
|
|
1811
|
+
425,
|
|
1812
|
+
429,
|
|
1813
|
+
500,
|
|
1814
|
+
502,
|
|
1815
|
+
503,
|
|
1816
|
+
504
|
|
1817
|
+
]);
|
|
1818
|
+
|
|
1819
|
+
// src/lib/session/retry/follow-retry-delay.ts
|
|
1820
|
+
function followRetryDelay(error, failures, random) {
|
|
1821
|
+
const exponential = Math.min(
|
|
1822
|
+
FOLLOW_RETRY_CAP_SECONDS,
|
|
1823
|
+
2 ** Math.min(failures - 1, 30)
|
|
1824
|
+
);
|
|
1825
|
+
const jittered = exponential / 2 + random() * (exponential / 2);
|
|
1826
|
+
const retryAfter = Math.max(0, error.details.retryAfterSeconds ?? 0);
|
|
1827
|
+
return Math.max(jittered, retryAfter + random() * (exponential / 2));
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// src/lib/session/retry/remaining-outage-seconds.ts
|
|
1831
|
+
function remainingOutageSeconds(outageStartedAt, now, maxOutageMs) {
|
|
1832
|
+
if (maxOutageMs === null) {
|
|
1833
|
+
return Number.POSITIVE_INFINITY;
|
|
1834
|
+
}
|
|
1835
|
+
return (maxOutageMs - (now - outageStartedAt)) / 1e3;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// src/lib/session/retry/retryable-follow-error.ts
|
|
1839
|
+
function retryableFollowError(error) {
|
|
1840
|
+
return error instanceof CliError && (RETRYABLE_FOLLOW_STATUS.has(error.details.status ?? 0) || error.details.code === CLI_ERROR_CODES.NETWORK || error.details.code === CLI_ERROR_CODES.TIMEOUT);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// src/lib/session/retry/should-report-follow-outage.ts
|
|
1844
|
+
function shouldReportFollowOutage(lastWarnedAt, now) {
|
|
1845
|
+
return lastWarnedAt === null || now - lastWarnedAt >= FOLLOW_OUTAGE_FEEDBACK_MS;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// src/lib/session/retry/wait-for-follow-retry.ts
|
|
1849
|
+
async function waitForFollowRetry(params) {
|
|
1850
|
+
const { error, retry, dependencies } = params;
|
|
1851
|
+
if (!retryableFollowError(error)) {
|
|
1852
|
+
throw error;
|
|
1853
|
+
}
|
|
1854
|
+
retry.failures += 1;
|
|
1855
|
+
const now = dependencies.now();
|
|
1856
|
+
retry.outageStartedAt ??= now;
|
|
1857
|
+
const delay = nextPollDelay(
|
|
1858
|
+
followRetryDelay(error, retry.failures, dependencies.random),
|
|
1859
|
+
remainingOutageSeconds(retry.outageStartedAt, now, params.maxOutageMs)
|
|
1860
|
+
);
|
|
1861
|
+
if (delay === null) {
|
|
1862
|
+
throw error;
|
|
1863
|
+
}
|
|
1864
|
+
if (shouldReportFollowOutage(retry.lastWarnedAt, now)) {
|
|
1865
|
+
dependencies.warn(followOutageMessage(retry.warned));
|
|
1866
|
+
retry.warned = true;
|
|
1867
|
+
retry.lastWarnedAt = now;
|
|
1868
|
+
}
|
|
1869
|
+
await dependencies.retrySleep(delay);
|
|
1870
|
+
if (remainingOutageSeconds(
|
|
1871
|
+
retry.outageStartedAt,
|
|
1872
|
+
dependencies.now(),
|
|
1873
|
+
params.maxOutageMs
|
|
1874
|
+
) <= 0) {
|
|
1875
|
+
throw error;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// src/lib/session/retry/request-with-follow-retry.ts
|
|
1880
|
+
async function requestWithFollowRetry(params) {
|
|
1881
|
+
for (; ; ) {
|
|
1882
|
+
try {
|
|
1883
|
+
const result = await params.operation();
|
|
1884
|
+
if (params.retry.warned) {
|
|
1885
|
+
params.dependencies.warn("Hepha API recovered; continuing follow.");
|
|
1886
|
+
}
|
|
1887
|
+
params.retry.failures = 0;
|
|
1888
|
+
params.retry.warned = false;
|
|
1889
|
+
params.retry.lastWarnedAt = null;
|
|
1890
|
+
params.retry.outageStartedAt = null;
|
|
1891
|
+
return result;
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
await waitForFollowRetry({
|
|
1894
|
+
error,
|
|
1895
|
+
retry: params.retry,
|
|
1896
|
+
maxOutageMs: params.maxOutageMs,
|
|
1897
|
+
dependencies: params.dependencies
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1744
1902
|
|
|
1745
1903
|
// src/lib/blocked/blocked-constants.ts
|
|
1746
1904
|
var BLOCKED = /* @__PURE__ */ new Set(["waiting_human", "awaiting_approval"]);
|
|
1747
1905
|
|
|
1748
|
-
// src/lib/
|
|
1749
|
-
|
|
1750
|
-
|
|
1906
|
+
// src/lib/terminal/terminal-constants.ts
|
|
1907
|
+
var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "killed", "abstained"]);
|
|
1908
|
+
|
|
1909
|
+
// src/lib/session/is-follow-stop-status.ts
|
|
1910
|
+
function isFollowStopStatus(status) {
|
|
1911
|
+
return status !== void 0 && (TERMINAL.has(status) || BLOCKED.has(status));
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// src/lib/session/retry/resolve-follow-outage-budget.ts
|
|
1915
|
+
function resolveFollowOutageBudget() {
|
|
1916
|
+
const raw = process.env.HEPHA_FOLLOW_MAX_OUTAGE_SECONDS?.trim();
|
|
1917
|
+
if (!raw) {
|
|
1918
|
+
return null;
|
|
1919
|
+
}
|
|
1920
|
+
const budgetMs = Number(raw) * 1e3;
|
|
1921
|
+
if (!Number.isFinite(budgetMs) || budgetMs <= 0) {
|
|
1922
|
+
throw new CliError(
|
|
1923
|
+
`HEPHA_FOLLOW_MAX_OUTAGE_SECONDS expects a positive number of seconds, got "${raw}".`,
|
|
1924
|
+
2
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
return budgetMs;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// src/lib/session/retry/follow-request-timeout-ms.ts
|
|
1931
|
+
function followRequestTimeoutMs(retry, maxOutageMs, now) {
|
|
1932
|
+
if (retry.outageStartedAt === null) {
|
|
1933
|
+
return resolveTimeout();
|
|
1934
|
+
}
|
|
1935
|
+
const remainingMs = remainingOutageSeconds(retry.outageStartedAt, now, maxOutageMs) * 1e3;
|
|
1936
|
+
return Math.max(1, Math.min(resolveTimeout(), remainingMs));
|
|
1751
1937
|
}
|
|
1752
1938
|
|
|
1753
1939
|
// src/lib/session/follow-session.ts
|
|
1754
|
-
async function followSession(sessionId, options = {}) {
|
|
1940
|
+
async function followSession(sessionId, options = {}, dependencies = defaultFollowSessionDependencies) {
|
|
1755
1941
|
let cursor = options.after ?? 0;
|
|
1942
|
+
let emptyPolls = 0;
|
|
1943
|
+
const retry = {
|
|
1944
|
+
failures: 0,
|
|
1945
|
+
warned: false,
|
|
1946
|
+
lastWarnedAt: null,
|
|
1947
|
+
outageStartedAt: null
|
|
1948
|
+
};
|
|
1949
|
+
const maxOutageMs = resolveFollowOutageBudget();
|
|
1756
1950
|
for (; ; ) {
|
|
1757
|
-
const page =
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1951
|
+
const page = await requestWithFollowRetry({
|
|
1952
|
+
retry,
|
|
1953
|
+
maxOutageMs,
|
|
1954
|
+
dependencies,
|
|
1955
|
+
operation: async () => expectShape(
|
|
1956
|
+
await dependencies.request(path`/sessions/${sessionId}/events`, {
|
|
1957
|
+
query: { after: cursor || void 0, limit: PAGE_SIZE },
|
|
1958
|
+
timeoutMs: followRequestTimeoutMs(
|
|
1959
|
+
retry,
|
|
1960
|
+
maxOutageMs,
|
|
1961
|
+
dependencies.now()
|
|
1962
|
+
)
|
|
1963
|
+
}),
|
|
1964
|
+
"GET /sessions/{id}/events",
|
|
1965
|
+
{
|
|
1966
|
+
items: is.array,
|
|
1967
|
+
hasMore: (value) => typeof value === "boolean",
|
|
1968
|
+
nextCursor: (value) => value === null || is.number(value),
|
|
1969
|
+
// Optional: a deployment older than this binary omits it, and a
|
|
1970
|
+
// missing field here would be a non-retryable schema mismatch.
|
|
1971
|
+
status: is.optionalString
|
|
1972
|
+
}
|
|
1973
|
+
)
|
|
1974
|
+
});
|
|
1764
1975
|
cursor = consumeSessionEvents(page.items, cursor, options.quiet);
|
|
1765
1976
|
if (page.nextCursor !== null) {
|
|
1766
1977
|
cursor = Math.max(cursor, page.nextCursor);
|
|
@@ -1768,11 +1979,30 @@ async function followSession(sessionId, options = {}) {
|
|
|
1768
1979
|
if (page.hasMore) {
|
|
1769
1980
|
continue;
|
|
1770
1981
|
}
|
|
1771
|
-
|
|
1772
|
-
if (
|
|
1773
|
-
|
|
1982
|
+
emptyPolls = page.items.length === 0 ? emptyPolls + 1 : 0;
|
|
1983
|
+
if (isFollowStopStatus(page.status) || page.items.length > 0 || emptyPolls >= 3) {
|
|
1984
|
+
const session = await requestWithFollowRetry({
|
|
1985
|
+
retry,
|
|
1986
|
+
maxOutageMs,
|
|
1987
|
+
dependencies,
|
|
1988
|
+
operation: async () => expectShape(
|
|
1989
|
+
await dependencies.request(path`/sessions/${sessionId}`, {
|
|
1990
|
+
timeoutMs: followRequestTimeoutMs(
|
|
1991
|
+
retry,
|
|
1992
|
+
maxOutageMs,
|
|
1993
|
+
dependencies.now()
|
|
1994
|
+
)
|
|
1995
|
+
}),
|
|
1996
|
+
"GET /sessions/{id}",
|
|
1997
|
+
{ status: is.string }
|
|
1998
|
+
)
|
|
1999
|
+
});
|
|
2000
|
+
emptyPolls = 0;
|
|
2001
|
+
if (isFollowStopStatus(session.status)) {
|
|
2002
|
+
return session;
|
|
2003
|
+
}
|
|
1774
2004
|
}
|
|
1775
|
-
await
|
|
2005
|
+
await dependencies.pollSleep(POLL_SECONDS);
|
|
1776
2006
|
}
|
|
1777
2007
|
}
|
|
1778
2008
|
|