pikiloom 0.4.52 → 0.4.54
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/dashboard/dist/assets/{AgentTab-TSZ8xlgI.js → AgentTab-DJfd-aVj.js} +1 -1
- package/dashboard/dist/assets/{ConnectionModal-B9oO45Ef.js → ConnectionModal-DyybJ5vd.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-C_WsxNQt.js → DirBrowser-BeFl4pxe.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-BnjedgD_.js → ExtensionsTab-VUqUXTUU.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-BSd7MmZS.js → IMAccessTab-x5c4G5ck.js} +1 -1
- package/dashboard/dist/assets/{Modal-DqFyZZqT.js → Modal-CVov70Z-.js} +1 -1
- package/dashboard/dist/assets/{Modals-BJYW-XQN.js → Modals-Cru4ScU4.js} +1 -1
- package/dashboard/dist/assets/{Select-CSadvVMI.js → Select-D39-GxSd.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-CryozDR-.js +1 -0
- package/dashboard/dist/assets/{SystemTab-q3hvXi8s.js → SystemTab-C1SNkUop.js} +1 -1
- package/dashboard/dist/assets/index-89WIHUvA.js +3 -0
- package/dashboard/dist/assets/index-BQ_R1F_G.css +1 -0
- package/dashboard/dist/assets/{index-DWfL0h2E.js → index-BTlkZmgz.js} +15 -15
- package/dashboard/dist/assets/{shared-CCX1F8yo.js → shared-CpIdR9QI.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/drivers/claude.js +164 -33
- package/dist/agent/drivers/codex.js +109 -26
- package/dist/agent/kernel-bridge.js +32 -12
- package/dist/agent/utils.js +5 -1
- package/dist/bot/bot.js +18 -1
- package/dist/bot/render-shared.js +15 -2
- package/dist/core/turn-audit.js +25 -0
- package/package.json +1 -1
- package/packages/kernel/dist/drivers/claude.d.ts +3 -0
- package/packages/kernel/dist/drivers/claude.js +90 -6
- package/packages/kernel/dist/index.d.ts +1 -1
- package/packages/kernel/dist/index.js +1 -1
- package/dashboard/dist/assets/SessionPanel-xQtNHsyz.js +0 -1
- package/dashboard/dist/assets/index-BcPBJtn2.css +0 -1
- package/dashboard/dist/assets/index-FePQwr1a.js +0 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as s}from"./react-vendor-C7Sl8SE7.js";import{O as t,m as i,S as a}from"./index-
|
|
1
|
+
import{j as s}from"./react-vendor-C7Sl8SE7.js";import{O as t,m as i,S as a}from"./index-89WIHUvA.js";function o({primary:e,secondary:l,tertiary:n}){return s.jsxs("div",{className:"flex flex-col gap-2 pt-1 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between",children:[s.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e&&s.jsxs(i,{tone:"primary",onClick:e.onClick,disabled:e.disabled,children:[e.loading&&s.jsx(a,{}),e.label]}),l&&s.jsx(i,{tone:"secondary",onClick:l.onClick,disabled:l.disabled,children:l.label})]}),n&&s.jsx("div",{className:"text-xs leading-relaxed text-fg-4",children:n})]})}function c({children:e,className:l}){return s.jsx(t,{padding:"md",elevation:"flat",className:l,children:e})}export{o as A,c as S};
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
<link rel="icon" type="image/png" href="/logo.png">
|
|
7
7
|
<title>Pikiloom</title>
|
|
8
8
|
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-89WIHUvA.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/react-vendor-C7Sl8SE7.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/router-DHISdpPk.js">
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BQ_R1F_G.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -1440,13 +1440,21 @@ const SYSTEM_INJECTED_USER_TAGS = new Set([
|
|
|
1440
1440
|
'case_id',
|
|
1441
1441
|
'tool-use-id',
|
|
1442
1442
|
'output-file',
|
|
1443
|
+
// Kernel claude driver's in-process truncated-turn recovery prompt (see packages/kernel
|
|
1444
|
+
// drivers/claude.ts): injected on the CLI's stdin, so it lands in the jsonl as a real user
|
|
1445
|
+
// message — hide it from the transcript like other system-injected turns.
|
|
1446
|
+
'pikiloom-recover',
|
|
1443
1447
|
]);
|
|
1448
|
+
// The CLI's resume-time repair placeholder for a turn that never concluded (paired with an
|
|
1449
|
+
// isMeta "Continue from where you left off." user record). It is not model output — but it IS
|
|
1450
|
+
// the only durable marker that the previous reply was cut off before its closing message.
|
|
1444
1451
|
function isClaudeSyntheticResumeNoise(text) {
|
|
1445
1452
|
const t = (text || '').trim().toLowerCase();
|
|
1446
1453
|
if (!t)
|
|
1447
1454
|
return true;
|
|
1448
1455
|
return t === 'no response requested.' || t === 'no response requested';
|
|
1449
1456
|
}
|
|
1457
|
+
const CLAUDE_INCOMPLETE_TURN_NOTICE = '⚠️ This reply ended before a closing message was delivered (interrupted, or the model returned an empty final response).';
|
|
1450
1458
|
function isSystemInjectedUserEvent(text) {
|
|
1451
1459
|
const trimmed = (text || '').trim();
|
|
1452
1460
|
if (!trimmed)
|
|
@@ -1629,13 +1637,18 @@ function getClaudeSessionMessages(opts) {
|
|
|
1629
1637
|
else if (ev.type === 'assistant') {
|
|
1630
1638
|
if (ev.message?.model === '<synthetic>') {
|
|
1631
1639
|
const noticeText = extractClaudeText(ev.message?.content, true).trim();
|
|
1632
|
-
|
|
1640
|
+
// "No response requested." is the CLI's resume repair for a turn that never
|
|
1641
|
+
// concluded. Dropping it hid the cut-off entirely — the dangling turn read as a
|
|
1642
|
+
// normal answer that stops mid-sentence. Show a notice on that turn instead.
|
|
1643
|
+
const displayText = !noticeText
|
|
1644
|
+
? ''
|
|
1645
|
+
: isClaudeSyntheticResumeNoise(noticeText) ? CLAUDE_INCOMPLETE_TURN_NOTICE : noticeText;
|
|
1646
|
+
if (!displayText)
|
|
1633
1647
|
continue;
|
|
1634
1648
|
if (pendingRole === 'user')
|
|
1635
1649
|
flush();
|
|
1636
1650
|
pendingRole = 'assistant';
|
|
1637
|
-
|
|
1638
|
-
pendingBlocks.push({ type: 'system_notice', content: noticeText });
|
|
1651
|
+
pendingBlocks.push({ type: 'system_notice', content: displayText });
|
|
1639
1652
|
continue;
|
|
1640
1653
|
}
|
|
1641
1654
|
if (pendingRole === 'user')
|
|
@@ -1808,37 +1821,137 @@ function getClaudeOAuthToken() {
|
|
|
1808
1821
|
return null;
|
|
1809
1822
|
}
|
|
1810
1823
|
}
|
|
1824
|
+
function claudeResetInfo(resetsAt) {
|
|
1825
|
+
const resetAt = typeof resetsAt === 'string' ? resetsAt : null;
|
|
1826
|
+
let resetAfterSeconds = null;
|
|
1827
|
+
if (resetAt) {
|
|
1828
|
+
const resetAtMs = Date.parse(resetAt);
|
|
1829
|
+
if (Number.isFinite(resetAtMs))
|
|
1830
|
+
resetAfterSeconds = Math.max(0, Math.round((resetAtMs - Date.now()) / 1000));
|
|
1831
|
+
}
|
|
1832
|
+
return { resetAt, resetAfterSeconds };
|
|
1833
|
+
}
|
|
1834
|
+
// Window status from the upstream severity when it maps to one of ours, else from the percent.
|
|
1835
|
+
function claudeWindowStatus(usedPercent, severity) {
|
|
1836
|
+
const fromSeverity = normalizeUsageStatus(severity);
|
|
1837
|
+
if (fromSeverity === 'limit_reached' || fromSeverity === 'warning' || fromSeverity === 'allowed')
|
|
1838
|
+
return fromSeverity;
|
|
1839
|
+
return usedPercent >= 100 ? 'limit_reached' : usedPercent >= 80 ? 'warning' : 'allowed';
|
|
1840
|
+
}
|
|
1841
|
+
// "$101.61" from a minor-unit amount (10161 with exponent 2). Non-USD keeps the currency code.
|
|
1842
|
+
function claudeCreditAmount(amountMinor, exponent, currency) {
|
|
1843
|
+
const minor = Number(amountMinor);
|
|
1844
|
+
if (!Number.isFinite(minor))
|
|
1845
|
+
return null;
|
|
1846
|
+
const expRaw = Number(exponent);
|
|
1847
|
+
const exp = Number.isFinite(expRaw) ? Math.max(0, Math.min(4, Math.round(expRaw))) : 2;
|
|
1848
|
+
const value = (minor / Math.pow(10, exp)).toFixed(exp);
|
|
1849
|
+
const cur = typeof currency === 'string' && currency ? currency.toUpperCase() : 'USD';
|
|
1850
|
+
return cur === 'USD' ? `$${value}` : `${value} ${cur}`;
|
|
1851
|
+
}
|
|
1852
|
+
const CLAUDE_LIMIT_KIND_LABELS = {
|
|
1853
|
+
session: '5h',
|
|
1854
|
+
weekly_all: '7d',
|
|
1855
|
+
};
|
|
1856
|
+
// One window per `limits[]` entry. `weekly_scoped` is the per-model weekly quota (the successor
|
|
1857
|
+
// of the legacy seven_day_opus / seven_day_sonnet keys) — label it by the scoped model name.
|
|
1858
|
+
function claudeWindowFromLimitEntry(entry) {
|
|
1859
|
+
if (!entry || typeof entry !== 'object')
|
|
1860
|
+
return null;
|
|
1861
|
+
const usedPercent = roundPercent(entry.percent);
|
|
1862
|
+
if (usedPercent == null)
|
|
1863
|
+
return null;
|
|
1864
|
+
const kind = typeof entry.kind === 'string' ? entry.kind : '';
|
|
1865
|
+
let label = CLAUDE_LIMIT_KIND_LABELS[kind] ?? null;
|
|
1866
|
+
if (!label && kind === 'weekly_scoped') {
|
|
1867
|
+
const scopeName = entry.scope?.model?.display_name || entry.scope?.model?.id || entry.scope?.surface;
|
|
1868
|
+
label = typeof scopeName === 'string' && scopeName ? `7d ${scopeName}` : '7d scoped';
|
|
1869
|
+
}
|
|
1870
|
+
if (!label)
|
|
1871
|
+
label = kind ? kind.replace(/_/g, ' ') : 'limit';
|
|
1872
|
+
const { resetAt, resetAfterSeconds } = claudeResetInfo(entry.resets_at);
|
|
1873
|
+
return {
|
|
1874
|
+
label, usedPercent,
|
|
1875
|
+
remainingPercent: Math.max(0, Math.round((100 - usedPercent) * 10) / 10),
|
|
1876
|
+
resetAt, resetAfterSeconds,
|
|
1877
|
+
status: claudeWindowStatus(usedPercent, entry.severity),
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
// The Extra (credit-metered overage) window. Percent comes from extra_usage.utilization (finer
|
|
1881
|
+
// grained) or spend.percent; the actual spend money rides along as `detail` so surfaces can show
|
|
1882
|
+
// "$101.61 / $500.00" instead of a bare percent. Skipped when the account has Extra disabled.
|
|
1883
|
+
function claudeExtraUsageWindow(data) {
|
|
1884
|
+
const extra = data?.extra_usage && typeof data.extra_usage === 'object' ? data.extra_usage : null;
|
|
1885
|
+
const spend = data?.spend && typeof data.spend === 'object' ? data.spend : null;
|
|
1886
|
+
if (extra?.is_enabled === false || (!extra && spend?.enabled === false))
|
|
1887
|
+
return null;
|
|
1888
|
+
let usedPercent = extra ? roundPercent(extra.utilization) : null;
|
|
1889
|
+
if (usedPercent == null && spend)
|
|
1890
|
+
usedPercent = roundPercent(spend.percent);
|
|
1891
|
+
if (usedPercent == null)
|
|
1892
|
+
return null;
|
|
1893
|
+
let detail = null;
|
|
1894
|
+
const used = claudeCreditAmount(spend?.used?.amount_minor, spend?.used?.exponent, spend?.used?.currency)
|
|
1895
|
+
?? claudeCreditAmount(extra?.used_credits, extra?.decimal_places, extra?.currency);
|
|
1896
|
+
const limit = claudeCreditAmount(spend?.limit?.amount_minor, spend?.limit?.exponent, spend?.limit?.currency)
|
|
1897
|
+
?? claudeCreditAmount(extra?.monthly_limit, extra?.decimal_places, extra?.currency);
|
|
1898
|
+
if (used)
|
|
1899
|
+
detail = limit ? `${used} / ${limit}` : used;
|
|
1900
|
+
const { resetAt, resetAfterSeconds } = claudeResetInfo(extra?.resets_at);
|
|
1901
|
+
return {
|
|
1902
|
+
label: 'Extra', usedPercent,
|
|
1903
|
+
remainingPercent: Math.max(0, Math.round((100 - usedPercent) * 10) / 10),
|
|
1904
|
+
resetAt, resetAfterSeconds,
|
|
1905
|
+
status: claudeWindowStatus(usedPercent, spend?.severity),
|
|
1906
|
+
detail,
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1811
1909
|
// Shape the `/api/oauth/usage` JSON into a UsageResult (shared by the sync curl probe and the
|
|
1812
1910
|
// async fetch probe). Returns null on an API error payload or when no usable window is present.
|
|
1813
|
-
|
|
1911
|
+
//
|
|
1912
|
+
// The payload carries two generations of quota shapes and we prefer the newer one:
|
|
1913
|
+
// - `limits[]` (2026+): one entry per window — kind "session" (5h), "weekly_all" (7d) and
|
|
1914
|
+
// "weekly_scoped" (per-model weekly, scope.model.display_name) — each with its own percent /
|
|
1915
|
+
// severity / resets_at. On these payloads the legacy `seven_day_opus` / `seven_day_sonnet`
|
|
1916
|
+
// top-level keys are null, so parsing only those silently drops the scoped model window.
|
|
1917
|
+
// - legacy fixed top-level keys: five_hour / seven_day / seven_day_opus / seven_day_sonnet.
|
|
1918
|
+
// `extra_usage` + `spend` (credit-metered overage) ride on both shapes, handled separately so
|
|
1919
|
+
// the actual dollar spend is preserved.
|
|
1920
|
+
export function buildClaudeOAuthUsage(data) {
|
|
1814
1921
|
const apiError = data?.error;
|
|
1815
1922
|
if (apiError && typeof apiError === 'object')
|
|
1816
1923
|
return null;
|
|
1817
|
-
const makeWindow = (label, entry) => {
|
|
1818
|
-
if (!entry || typeof entry !== 'object')
|
|
1819
|
-
return null;
|
|
1820
|
-
const usedPercent = roundPercent(entry.utilization);
|
|
1821
|
-
if (usedPercent == null)
|
|
1822
|
-
return null;
|
|
1823
|
-
const remainingPercent = Math.max(0, Math.round((100 - usedPercent) * 10) / 10);
|
|
1824
|
-
const resetAt = typeof entry.resets_at === 'string' ? entry.resets_at : null;
|
|
1825
|
-
let resetAfterSeconds = null;
|
|
1826
|
-
if (resetAt) {
|
|
1827
|
-
const resetAtMs = Date.parse(resetAt);
|
|
1828
|
-
if (Number.isFinite(resetAtMs))
|
|
1829
|
-
resetAfterSeconds = Math.max(0, Math.round((resetAtMs - Date.now()) / 1000));
|
|
1830
|
-
}
|
|
1831
|
-
return {
|
|
1832
|
-
label, usedPercent, remainingPercent, resetAt, resetAfterSeconds,
|
|
1833
|
-
status: usedPercent >= 100 ? 'limit_reached' : usedPercent >= 80 ? 'warning' : 'allowed',
|
|
1834
|
-
};
|
|
1835
|
-
};
|
|
1836
1924
|
const windows = [];
|
|
1837
|
-
|
|
1838
|
-
|
|
1925
|
+
const limitEntries = Array.isArray(data?.limits) ? data.limits : [];
|
|
1926
|
+
for (const entry of limitEntries) {
|
|
1927
|
+
const w = claudeWindowFromLimitEntry(entry);
|
|
1839
1928
|
if (w)
|
|
1840
1929
|
windows.push(w);
|
|
1841
1930
|
}
|
|
1931
|
+
if (!windows.length) {
|
|
1932
|
+
const makeWindow = (label, entry) => {
|
|
1933
|
+
if (!entry || typeof entry !== 'object')
|
|
1934
|
+
return null;
|
|
1935
|
+
const usedPercent = roundPercent(entry.utilization);
|
|
1936
|
+
if (usedPercent == null)
|
|
1937
|
+
return null;
|
|
1938
|
+
const { resetAt, resetAfterSeconds } = claudeResetInfo(entry.resets_at);
|
|
1939
|
+
return {
|
|
1940
|
+
label, usedPercent,
|
|
1941
|
+
remainingPercent: Math.max(0, Math.round((100 - usedPercent) * 10) / 10),
|
|
1942
|
+
resetAt, resetAfterSeconds,
|
|
1943
|
+
status: claudeWindowStatus(usedPercent),
|
|
1944
|
+
};
|
|
1945
|
+
};
|
|
1946
|
+
for (const [label, key] of [['5h', 'five_hour'], ['7d', 'seven_day'], ['7d Opus', 'seven_day_opus'], ['7d Sonnet', 'seven_day_sonnet']]) {
|
|
1947
|
+
const w = makeWindow(label, data[key]);
|
|
1948
|
+
if (w)
|
|
1949
|
+
windows.push(w);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
const extra = claudeExtraUsageWindow(data);
|
|
1953
|
+
if (extra)
|
|
1954
|
+
windows.push(extra);
|
|
1842
1955
|
if (!windows.length)
|
|
1843
1956
|
return null;
|
|
1844
1957
|
const overallStatus = windows.some(w => w.status === 'limit_reached') ? 'limit_reached'
|
|
@@ -1892,6 +2005,13 @@ async function fetchClaudeUsageFromOAuth(tokenOverride) {
|
|
|
1892
2005
|
// one request. `force` is for explicit user-clicked refreshes only: it bypasses both the TTL and
|
|
1893
2006
|
// the failure backoff (human-paced clicks cannot stampede the endpoint), with a short floor to
|
|
1894
2007
|
// absorb double-clicks.
|
|
2008
|
+
//
|
|
2009
|
+
// The 429 budget is PER ACCOUNT and shared with every other Claude Code process on the machine,
|
|
2010
|
+
// so the endpoint can stay rate-limited for long stretches through no fault of our cadence. When
|
|
2011
|
+
// it fails and there is no last-good to serve, degrade to the inference-header probe (the same
|
|
2012
|
+
// channel setup-token accounts use): coarser — 5h/7d only, no Extra spend or per-model weekly —
|
|
2013
|
+
// but reliably available, so the native row never goes blank. A later successful oauth read
|
|
2014
|
+
// replaces it with the full-fidelity snapshot.
|
|
1895
2015
|
const NATIVE_USAGE_FRESH_TTL_MS = 15_000;
|
|
1896
2016
|
const NATIVE_USAGE_RETRY_TTL_MS = 60_000;
|
|
1897
2017
|
const NATIVE_USAGE_FORCE_FLOOR_MS = 3_000;
|
|
@@ -1909,20 +2029,31 @@ export function claudeNativeUsage(opts) {
|
|
|
1909
2029
|
}
|
|
1910
2030
|
if (claudeNativeUsageLive.inflight)
|
|
1911
2031
|
return claudeNativeUsageLive.inflight;
|
|
1912
|
-
const p =
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2032
|
+
const p = (async () => {
|
|
2033
|
+
let result = await fetchClaudeUsageFromOAuth();
|
|
2034
|
+
const oauthOk = !!result;
|
|
2035
|
+
// Degrade to the header probe when oauth fails and there is no full-fidelity snapshot to
|
|
2036
|
+
// serve — including when last-good is itself a header-probe result (keep it refreshing
|
|
2037
|
+
// through a long 429 stretch instead of freezing on the first degraded read).
|
|
2038
|
+
if (!result && (!claudeUsageCache.lastGood || claudeUsageCache.lastGood.source === 'ratelimit-headers')) {
|
|
2039
|
+
const token = getClaudeOAuthToken();
|
|
2040
|
+
if (token)
|
|
2041
|
+
result = await claudeUsageForToken(token, opts);
|
|
2042
|
+
agentLog(`[usage] native oauth usage read failed; header-probe fallback -> ${result ? result.source : 'null'}`);
|
|
2043
|
+
}
|
|
2044
|
+
if (result) {
|
|
2045
|
+
claudeUsageCache.lastGood = result;
|
|
1916
2046
|
claudeUsageCache.lastAttemptAt = Date.now();
|
|
1917
2047
|
claudeNativeUsageLive.at = Date.now();
|
|
1918
|
-
|
|
2048
|
+
// A header-probe fallback is a degraded read: keep failedAt so the oauth endpoint is
|
|
2049
|
+
// retried on the normal backoff cadence instead of idling out the full ok-TTL.
|
|
2050
|
+
claudeNativeUsageLive.failedAt = oauthOk ? 0 : Date.now();
|
|
1919
2051
|
}
|
|
1920
2052
|
else {
|
|
1921
2053
|
claudeNativeUsageLive.failedAt = Date.now();
|
|
1922
2054
|
}
|
|
1923
|
-
return
|
|
1924
|
-
})
|
|
1925
|
-
.finally(() => { claudeNativeUsageLive.inflight = null; });
|
|
2055
|
+
return result ?? claudeUsageCache.lastGood;
|
|
2056
|
+
})().finally(() => { claudeNativeUsageLive.inflight = null; });
|
|
1926
2057
|
claudeNativeUsageLive.inflight = p;
|
|
1927
2058
|
return p;
|
|
1928
2059
|
}
|
|
@@ -1882,22 +1882,61 @@ function getCodexStateDbPath(home) {
|
|
|
1882
1882
|
return null;
|
|
1883
1883
|
}
|
|
1884
1884
|
}
|
|
1885
|
-
|
|
1885
|
+
// Account status across both codex rate-limit generations: newer payloads report
|
|
1886
|
+
// `rate_limit_reached_type` (a string names the tripped window, null means not limited); older
|
|
1887
|
+
// ones used `limit_reached` / `allowed` booleans, which no longer exist on current codex.
|
|
1888
|
+
function codexRateLimitStatus(rateLimits) {
|
|
1889
|
+
const reachedType = rateLimits.rate_limit_reached_type ?? rateLimits.rateLimitReachedType;
|
|
1890
|
+
if (typeof reachedType === 'string' && reachedType)
|
|
1891
|
+
return 'limit_reached';
|
|
1892
|
+
if (rateLimits.limit_reached === true)
|
|
1893
|
+
return 'limit_reached';
|
|
1894
|
+
if (rateLimits.allowed === true)
|
|
1895
|
+
return 'allowed';
|
|
1896
|
+
if ('rate_limit_reached_type' in rateLimits || 'rateLimitReachedType' in rateLimits)
|
|
1897
|
+
return 'allowed';
|
|
1898
|
+
return null;
|
|
1899
|
+
}
|
|
1900
|
+
// Compact credits description ("unlimited" or a balance figure); null when the credits block
|
|
1901
|
+
// carries no user-meaningful number (hasCredits alone says nothing about remaining spend).
|
|
1902
|
+
function codexCreditsSummary(credits) {
|
|
1903
|
+
if (!credits || typeof credits !== 'object')
|
|
1904
|
+
return null;
|
|
1905
|
+
if (credits.unlimited === true)
|
|
1906
|
+
return 'unlimited';
|
|
1907
|
+
const balance = credits.balance;
|
|
1908
|
+
if (typeof balance === 'number' && Number.isFinite(balance))
|
|
1909
|
+
return String(balance);
|
|
1910
|
+
if (typeof balance === 'string' && balance)
|
|
1911
|
+
return balance;
|
|
1912
|
+
return null;
|
|
1913
|
+
}
|
|
1914
|
+
export function codexUsageFromRateLimits(rateLimits, capturedAt, source) {
|
|
1886
1915
|
if (!rateLimits || typeof rateLimits !== 'object')
|
|
1887
1916
|
return null;
|
|
1888
1917
|
const windows = [
|
|
1889
1918
|
usageWindowFromRateLimit('Primary', rateLimits.primary),
|
|
1890
1919
|
usageWindowFromRateLimit('Secondary', rateLimits.secondary),
|
|
1891
1920
|
].filter((v) => !!v);
|
|
1921
|
+
// Team plans may carry a per-member limit alongside the shared one.
|
|
1922
|
+
const individual = rateLimits.individual_limit;
|
|
1923
|
+
if (individual && typeof individual === 'object') {
|
|
1924
|
+
for (const [fallback, entry] of [['Primary', individual.primary], ['Secondary', individual.secondary]]) {
|
|
1925
|
+
const w = usageWindowFromRateLimit(fallback, entry);
|
|
1926
|
+
if (w)
|
|
1927
|
+
windows.push({ ...w, label: `${w.label} (individual)` });
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1892
1930
|
if (!windows.length)
|
|
1893
1931
|
return null;
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
return { ok: true, agent: 'codex', source, capturedAt, status, windows, error: null };
|
|
1932
|
+
return {
|
|
1933
|
+
ok: true, agent: 'codex', source, capturedAt, status: codexRateLimitStatus(rateLimits), windows, error: null,
|
|
1934
|
+
planType: typeof rateLimits.plan_type === 'string' && rateLimits.plan_type ? rateLimits.plan_type : null,
|
|
1935
|
+
creditsSummary: codexCreditsSummary(rateLimits.credits),
|
|
1936
|
+
};
|
|
1900
1937
|
}
|
|
1938
|
+
// Legacy source: codex >= 0.142 dropped the `logs` table from the state db entirely, so this
|
|
1939
|
+
// returns null there (sqlite3 errors out) and callers fall through to the session-history scan.
|
|
1901
1940
|
function getCodexUsageFromStateDb(home) {
|
|
1902
1941
|
const dbPath = getCodexStateDbPath(home);
|
|
1903
1942
|
if (!dbPath)
|
|
@@ -1985,30 +2024,74 @@ function parseRateLimitWindow(label, rl) {
|
|
|
1985
2024
|
status: null,
|
|
1986
2025
|
};
|
|
1987
2026
|
}
|
|
2027
|
+
// Windows for one camelCase rate-limit block from the app-server: primary / secondary plus the
|
|
2028
|
+
// per-member individualLimit team plans may carry. `prefix` disambiguates window labels when an
|
|
2029
|
+
// account reports multiple limit ids.
|
|
2030
|
+
function codexLiveWindows(rl, prefix) {
|
|
2031
|
+
const windows = [];
|
|
2032
|
+
const push = (w, suffix = '') => {
|
|
2033
|
+
if (w)
|
|
2034
|
+
windows.push({ ...w, label: `${prefix}${w.label}${suffix}` });
|
|
2035
|
+
};
|
|
2036
|
+
push(parseRateLimitWindow('Primary', rl.primary));
|
|
2037
|
+
push(parseRateLimitWindow('Secondary', rl.secondary));
|
|
2038
|
+
const individual = rl.individualLimit;
|
|
2039
|
+
if (individual && typeof individual === 'object') {
|
|
2040
|
+
push(parseRateLimitWindow('Primary', individual.primary), ' (individual)');
|
|
2041
|
+
push(parseRateLimitWindow('Secondary', individual.secondary), ' (individual)');
|
|
2042
|
+
}
|
|
2043
|
+
return windows;
|
|
2044
|
+
}
|
|
2045
|
+
// Pure parse of an `account/rateLimits/read` result. Prefers `rateLimitsByLimitId` (accounts can
|
|
2046
|
+
// carry several limits) over the flat `rateLimits`; also lifts planType, credits and
|
|
2047
|
+
// `rateLimitResetCredits` (limit-reset coupons) which the previous parse dropped entirely.
|
|
2048
|
+
export function codexUsageFromLiveRateLimitsResult(result, capturedAt) {
|
|
2049
|
+
const byId = result?.rateLimitsByLimitId && typeof result.rateLimitsByLimitId === 'object'
|
|
2050
|
+
? Object.entries(result.rateLimitsByLimitId).filter(([, v]) => v && typeof v === 'object')
|
|
2051
|
+
: [];
|
|
2052
|
+
const blocks = byId.length ? byId
|
|
2053
|
+
: result?.rateLimits && typeof result.rateLimits === 'object' ? [['', result.rateLimits]] : [];
|
|
2054
|
+
if (!blocks.length)
|
|
2055
|
+
return null;
|
|
2056
|
+
const windows = [];
|
|
2057
|
+
let status = null;
|
|
2058
|
+
let planType = null;
|
|
2059
|
+
let creditsSummary = null;
|
|
2060
|
+
const multi = blocks.length > 1;
|
|
2061
|
+
for (const [limitId, rl] of blocks) {
|
|
2062
|
+
const name = typeof rl.limitName === 'string' && rl.limitName ? rl.limitName : limitId;
|
|
2063
|
+
windows.push(...codexLiveWindows(rl, multi && name ? `${name} ` : ''));
|
|
2064
|
+
const blockStatus = codexRateLimitStatus(rl);
|
|
2065
|
+
if (blockStatus === 'limit_reached')
|
|
2066
|
+
status = 'limit_reached';
|
|
2067
|
+
else if (blockStatus && status == null)
|
|
2068
|
+
status = blockStatus;
|
|
2069
|
+
if (!planType && typeof rl.planType === 'string' && rl.planType)
|
|
2070
|
+
planType = rl.planType;
|
|
2071
|
+
if (!creditsSummary)
|
|
2072
|
+
creditsSummary = codexCreditsSummary(rl.credits);
|
|
2073
|
+
}
|
|
2074
|
+
const resetCreditsRaw = Number(result?.rateLimitResetCredits?.availableCount);
|
|
2075
|
+
return {
|
|
2076
|
+
ok: windows.length > 0, agent: 'codex', source: 'app-server-live', capturedAt, status,
|
|
2077
|
+
windows, error: windows.length > 0 ? null : 'No rate limit windows.',
|
|
2078
|
+
planType, creditsSummary,
|
|
2079
|
+
resetCreditsAvailable: Number.isFinite(resetCreditsRaw) ? resetCreditsRaw : null,
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
1988
2082
|
export async function getCodexUsageLive() {
|
|
1989
2083
|
const home = getHome();
|
|
2084
|
+
// The state-db `logs` table no longer exists on codex >= 0.142, so the offline fallback must
|
|
2085
|
+
// also try the session-history scan.
|
|
2086
|
+
const fallback = (reason) => getCodexUsageFromStateDb(home) || getCodexUsageFromSessions(home) || emptyUsage('codex', reason);
|
|
1990
2087
|
const srv = getSharedServer();
|
|
1991
|
-
if (!(await srv.ensureRunning()))
|
|
1992
|
-
return
|
|
1993
|
-
}
|
|
2088
|
+
if (!(await srv.ensureRunning()))
|
|
2089
|
+
return fallback('Failed to start codex app-server.');
|
|
1994
2090
|
const resp = await srv.call('account/rateLimits/read');
|
|
1995
2091
|
if (resp.error)
|
|
1996
|
-
return
|
|
1997
|
-
const
|
|
1998
|
-
|
|
1999
|
-
return getCodexUsageFromStateDb(home) || emptyUsage('codex', 'No rate limits in response.');
|
|
2000
|
-
const capturedAt = new Date().toISOString();
|
|
2001
|
-
const windows = [];
|
|
2002
|
-
const w1 = parseRateLimitWindow('Primary', rl.primary);
|
|
2003
|
-
if (w1)
|
|
2004
|
-
windows.push(w1);
|
|
2005
|
-
const w2 = parseRateLimitWindow('Secondary', rl.secondary);
|
|
2006
|
-
if (w2)
|
|
2007
|
-
windows.push(w2);
|
|
2008
|
-
return {
|
|
2009
|
-
ok: windows.length > 0, agent: 'codex', source: 'app-server-live', capturedAt, status: null,
|
|
2010
|
-
windows, error: windows.length > 0 ? null : 'No rate limit windows.',
|
|
2011
|
-
};
|
|
2092
|
+
return fallback(resp.error.message || 'account/rateLimits/read failed');
|
|
2093
|
+
const parsed = codexUsageFromLiveRateLimitsResult(resp.result, new Date().toISOString());
|
|
2094
|
+
return parsed ?? fallback('No rate limits in response.');
|
|
2012
2095
|
}
|
|
2013
2096
|
class CodexDriver {
|
|
2014
2097
|
id = 'codex';
|
|
@@ -140,6 +140,30 @@ export function kernelUsageToResultFields(u) {
|
|
|
140
140
|
contextPercent: u?.contextPercent ?? null,
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
|
+
// How a kernel DriverResult presents as the finished message + incomplete flag. Pure +
|
|
144
|
+
// exported for regression testing. The load-bearing rule: a turn whose closing reply never
|
|
145
|
+
// arrived (stopReason 'stalled'/'truncated') must SAY so even when mid-turn narration exists —
|
|
146
|
+
// substituting a note only for empty text is exactly what let those endings read as normal
|
|
147
|
+
// answers that stop mid-sentence, with nothing marking the swallow.
|
|
148
|
+
export function composeKernelFinalPresentation(input) {
|
|
149
|
+
const endNote = input.stopReason === 'stalled'
|
|
150
|
+
? 'The turn stopped responding after tool use without producing a reply (the model or provider may have stalled). Send any message to continue.'
|
|
151
|
+
: input.stopReason === 'truncated'
|
|
152
|
+
? 'The reply ended after the last tool call without a closing message (the model returned an empty final response). Send any message to continue.'
|
|
153
|
+
: null;
|
|
154
|
+
// Empty-text fallback (mirrors the legacy driver): a clean turn with no prose reads
|
|
155
|
+
// "(no textual response)", not "(no output)" (which the kernel path used to show for every
|
|
156
|
+
// textless turn — including a background-launch turn that intentionally ends without prose).
|
|
157
|
+
// A turn that settled while work keeps running in the background gets an explicit note.
|
|
158
|
+
const message = input.bodyText
|
|
159
|
+
? (endNote ? `${input.bodyText}\n\n⚠️ ${endNote}` : input.bodyText)
|
|
160
|
+
: input.finalError
|
|
161
|
+
|| endNote
|
|
162
|
+
|| (input.stopReason === 'background'
|
|
163
|
+
? 'Work is now running in the background — I’ll report back here once it finishes. (Send any message to check on it.)'
|
|
164
|
+
: input.ok ? '(no textual response)' : '(no output)');
|
|
165
|
+
return { message, incomplete: !input.ok || !!endNote };
|
|
166
|
+
}
|
|
143
167
|
let _kernel = null;
|
|
144
168
|
export async function loadKernel() {
|
|
145
169
|
if (_kernel)
|
|
@@ -256,19 +280,15 @@ export async function kernelStream(opts) {
|
|
|
256
280
|
? (humanizeCodexError(result.error) ?? result.error)
|
|
257
281
|
: (result.error ?? null);
|
|
258
282
|
const usageFields = kernelUsageToResultFields(result.usage || snapshot.usage || {});
|
|
283
|
+
const presentation = composeKernelFinalPresentation({
|
|
284
|
+
bodyText: (result.text || snapshot.text || '').trim(),
|
|
285
|
+
finalError,
|
|
286
|
+
ok: !!result.ok,
|
|
287
|
+
stopReason: result.stopReason ?? null,
|
|
288
|
+
});
|
|
259
289
|
return {
|
|
260
290
|
ok: !!result.ok,
|
|
261
|
-
|
|
262
|
-
// "(no textual response)", not "(no output)" (which the kernel path used to show for every
|
|
263
|
-
// textless turn — including a background-launch turn that intentionally ends without prose).
|
|
264
|
-
// A turn that settled while work keeps running in the background gets an explicit note.
|
|
265
|
-
message: (result.text || snapshot.text || '').trim()
|
|
266
|
-
|| finalError
|
|
267
|
-
|| (result.stopReason === 'background'
|
|
268
|
-
? 'Work is now running in the background — I’ll report back here once it finishes. (Send any message to check on it.)'
|
|
269
|
-
: result.stopReason === 'stalled'
|
|
270
|
-
? 'The turn stopped responding after tool use without producing a reply (the model or provider may have stalled). Send any message to continue.'
|
|
271
|
-
: result.ok ? '(no textual response)' : '(no output)'),
|
|
291
|
+
message: presentation.message,
|
|
272
292
|
thinking: (result.reasoning || snapshot.reasoning || '').trim() || null,
|
|
273
293
|
plan: toPikiloomPlan(snapshot.plan),
|
|
274
294
|
sessionId: finalSessionId,
|
|
@@ -280,7 +300,7 @@ export async function kernelStream(opts) {
|
|
|
280
300
|
codexCumulative: null,
|
|
281
301
|
error: finalError,
|
|
282
302
|
stopReason: result.stopReason ?? null,
|
|
283
|
-
incomplete:
|
|
303
|
+
incomplete: presentation.incomplete,
|
|
284
304
|
activity: snapshot.activity || null,
|
|
285
305
|
};
|
|
286
306
|
}
|
package/dist/agent/utils.js
CHANGED
|
@@ -414,7 +414,7 @@ export function normalizeUsageStatus(value) {
|
|
|
414
414
|
return 'limit_reached';
|
|
415
415
|
if (normalized.includes('warning') || normalized.includes('warn'))
|
|
416
416
|
return 'warning';
|
|
417
|
-
if (normalized.includes('allowed') || normalized === 'ok' || normalized === 'healthy' || normalized === 'ready')
|
|
417
|
+
if (normalized.includes('allowed') || normalized === 'ok' || normalized === 'healthy' || normalized === 'ready' || normalized === 'normal')
|
|
418
418
|
return 'allowed';
|
|
419
419
|
return normalized;
|
|
420
420
|
}
|
|
@@ -427,6 +427,10 @@ export function labelFromWindowMinutes(value, fallback) {
|
|
|
427
427
|
return '5h';
|
|
428
428
|
if (Math.abs(roundedMinutes - 10080) <= 5)
|
|
429
429
|
return '7d';
|
|
430
|
+
// Codex team/enterprise plans meter on a monthly window reported as the average month
|
|
431
|
+
// length (365d/12 = 43800min = 730h) — without this branch it renders as "730h".
|
|
432
|
+
if (Math.abs(roundedMinutes - 43800) <= 60)
|
|
433
|
+
return '1mo';
|
|
430
434
|
const roundedDays = Math.round(roundedMinutes / 1440);
|
|
431
435
|
if (roundedDays >= 1 && Math.abs(roundedMinutes - roundedDays * 1440) <= 5)
|
|
432
436
|
return `${roundedDays}d`;
|
package/dist/bot/bot.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getDriver, hasDriver, allDriverIds, getDriverCapabilities } from '../ag
|
|
|
10
10
|
import { resolveGuiIntegrationConfig } from '../agent/mcp/bridge.js';
|
|
11
11
|
import { composeSessionToolPrompt } from '../agent/mcp/capabilities.js';
|
|
12
12
|
import { terminateProcessTree } from '../core/process-control.js';
|
|
13
|
+
import { appendTurnAudit } from '../core/turn-audit.js';
|
|
13
14
|
import { expandTilde } from '../core/platform.js';
|
|
14
15
|
import { VERSION } from '../core/version.js';
|
|
15
16
|
import { buildHumanLoopResponse, createEmptyHumanLoopAnswer, currentHumanLoopQuestion, isHumanLoopAwaitingText, setHumanLoopOption, setHumanLoopText, skipHumanLoopQuestion, summarizeResolvedHumanLoopAnswers, } from './human-loop.js';
|
|
@@ -2114,7 +2115,23 @@ export class Bot {
|
|
|
2114
2115
|
onCodexTurnReady,
|
|
2115
2116
|
forkOf: extras?.forkOf,
|
|
2116
2117
|
};
|
|
2117
|
-
|
|
2118
|
+
let result;
|
|
2119
|
+
try {
|
|
2120
|
+
result = await doStream(opts);
|
|
2121
|
+
}
|
|
2122
|
+
catch (e) {
|
|
2123
|
+
appendTurnAudit({
|
|
2124
|
+
agent: cs.agent, sessionId: cs.sessionId || null, ok: false, stopReason: 'exception',
|
|
2125
|
+
incomplete: true, error: String(e?.message || e).slice(0, 500), promptPreview: prompt.slice(0, 120),
|
|
2126
|
+
});
|
|
2127
|
+
throw e;
|
|
2128
|
+
}
|
|
2129
|
+
appendTurnAudit({
|
|
2130
|
+
agent: cs.agent, sessionId: result.sessionId || cs.sessionId || null,
|
|
2131
|
+
ok: result.ok, stopReason: result.stopReason ?? null, incomplete: !!result.incomplete,
|
|
2132
|
+
error: result.error ? String(result.error).slice(0, 500) : null,
|
|
2133
|
+
elapsedS: result.elapsedS, model: result.model ?? resolvedModel, promptPreview: prompt.slice(0, 120),
|
|
2134
|
+
});
|
|
2118
2135
|
if (cs.agent === 'claude' && workflowEnabled && result.thinkingEffort) {
|
|
2119
2136
|
result.thinkingEffort = 'ultra';
|
|
2120
2137
|
}
|
|
@@ -127,9 +127,22 @@ function formatUsageResetDuration(seconds) {
|
|
|
127
127
|
function formatUsageWindowSummary(window, now) {
|
|
128
128
|
const percent = window.usedPercent != null ? `${Math.round(window.usedPercent)}%` : null;
|
|
129
129
|
const reset = formatUsageWindowReset(window, now);
|
|
130
|
-
|
|
130
|
+
let main = percent ? `${window.label} ${percent}` : window.label;
|
|
131
|
+
if (window.detail)
|
|
132
|
+
main += ` (${window.detail})`;
|
|
131
133
|
return reset ? `${main} (${reset})` : main;
|
|
132
134
|
}
|
|
135
|
+
// "plan team · limit resets ×1" — account-level metadata riding on the usage snapshot.
|
|
136
|
+
function usageResultMetaParts(usage) {
|
|
137
|
+
const parts = [];
|
|
138
|
+
if (usage.planType)
|
|
139
|
+
parts.push(`plan ${usage.planType}`);
|
|
140
|
+
if (usage.creditsSummary)
|
|
141
|
+
parts.push(`credits ${usage.creditsSummary}`);
|
|
142
|
+
if (usage.resetCreditsAvailable)
|
|
143
|
+
parts.push(`limit resets ×${usage.resetCreditsAvailable}`);
|
|
144
|
+
return parts;
|
|
145
|
+
}
|
|
133
146
|
// Compact "5h 42% (reset 3h12m) · 7d 18% (reset 4d6h)" summary of a usage
|
|
134
147
|
// snapshot, or a short reason when no quota numbers are available.
|
|
135
148
|
export function formatUsageWindowsSummary(usage, now = Date.now()) {
|
|
@@ -139,7 +152,7 @@ export function formatUsageWindowsSummary(usage, now = Date.now()) {
|
|
|
139
152
|
.filter(w => w.usedPercent != null)
|
|
140
153
|
.map(w => formatUsageWindowSummary(w, now));
|
|
141
154
|
if (parts.length)
|
|
142
|
-
return parts.join(' · ');
|
|
155
|
+
return [...parts, ...usageResultMetaParts(usage)].join(' · ');
|
|
143
156
|
const resetWindow = usage.windows.find(w => usageWindowResetSeconds(w, now) != null);
|
|
144
157
|
if (resetWindow) {
|
|
145
158
|
const reset = formatUsageWindowReset(resetWindow, now);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getUserConfigPath } from './config/user-config.js';
|
|
4
|
+
// Per-turn终态 audit trail. Prod runs with no disk logs at all, which turns every
|
|
5
|
+
// swallowed-reply report into jsonl archaeology (reconstructing process lifetimes from MCP log
|
|
6
|
+
// filenames). One JSON line per finished turn — how it ended, not what it said — is enough to
|
|
7
|
+
// answer "who ended this turn and why" after the fact. Appends must never break a turn.
|
|
8
|
+
const MAX_AUDIT_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
export function turnAuditPath() {
|
|
10
|
+
return path.join(path.dirname(getUserConfigPath()), 'logs', 'turn-audit.jsonl');
|
|
11
|
+
}
|
|
12
|
+
export function appendTurnAudit(entry) {
|
|
13
|
+
try {
|
|
14
|
+
const file = turnAuditPath();
|
|
15
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
16
|
+
try {
|
|
17
|
+
// Single-slot rotation: cap the live file, keep exactly one predecessor.
|
|
18
|
+
if (fs.statSync(file).size > MAX_AUDIT_BYTES)
|
|
19
|
+
fs.renameSync(file, `${file}.1`);
|
|
20
|
+
}
|
|
21
|
+
catch { /* no file yet */ }
|
|
22
|
+
fs.appendFileSync(file, JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n');
|
|
23
|
+
}
|
|
24
|
+
catch { /* auditing is best-effort by design */ }
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -35,11 +35,14 @@ export declare function handleClaudeEvent(ev: any, s: any, emit: (e: DriverEvent
|
|
|
35
35
|
export declare function claudeBgHoldCapMs(): number;
|
|
36
36
|
export declare function claudeBgSettleQuietMs(): number;
|
|
37
37
|
export declare function claudeModelStallMs(): number;
|
|
38
|
+
export declare const CLAUDE_TRUNCATED_RECOVERY_PROMPT: string;
|
|
39
|
+
export declare function claudeTruncatedRecoveryEnabled(): boolean;
|
|
38
40
|
export declare function claudeUserEventHasToolResult(ev: any): boolean;
|
|
39
41
|
export declare function isTerminalTaskStatus(status: unknown): boolean;
|
|
40
42
|
export declare function trackClaudeBackgroundTask(ev: any, s: any): void;
|
|
41
43
|
export declare function markClaudeTaskNotificationTerminal(content: any, s: any): void;
|
|
42
44
|
export declare function pendingClaudeBackgroundTasks(s: any): number;
|
|
45
|
+
export declare function claudeTurnEndedDangling(s: any): boolean;
|
|
43
46
|
export type ClaudeResultSettleDecision = 'settle' | 'hold' | 'quiet-settle';
|
|
44
47
|
export declare function decideClaudeResultSettle(input: {
|
|
45
48
|
hasError: boolean;
|