pikiloom 0.4.41 → 0.4.43
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-B8V2eV_D.js +1 -0
- package/dashboard/dist/assets/{ConnectionModal-CAlACYKM.js → ConnectionModal-BOVwXLJB.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-D3KGIof1.js → DirBrowser-DbeAWYiL.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-C_69Y7CR.js → ExtensionsTab-BMS3PW9N.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-B5_6IS4X.js → IMAccessTab-B1sRgJk3.js} +1 -1
- package/dashboard/dist/assets/{Modal-BjZifpga.js → Modal-DCwGz46r.js} +1 -1
- package/dashboard/dist/assets/{Modals-B_V24pNA.js → Modals-Bh_0en5P.js} +1 -1
- package/dashboard/dist/assets/{Select-D7xW38wq.js → Select-DC6zguGC.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-DNlLd-PL.js +1 -0
- package/dashboard/dist/assets/{SystemTab-i5nmYweA.js → SystemTab-uFqZinIZ.js} +1 -1
- package/dashboard/dist/assets/index-CL5H13Cl.js +3 -0
- package/dashboard/dist/assets/index-DooLxbPX.js +23 -0
- package/dashboard/dist/assets/index-DwmXPtDd.css +1 -0
- package/dashboard/dist/assets/{shared-BKwEgcmy.js → shared-Byy2BNLq.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/accounts.js +181 -0
- package/dist/agent/drivers/claude.js +187 -36
- package/dist/agent/drivers/codex.js +30 -2
- package/dist/agent/kernel-bridge.js +63 -3
- package/dist/agent/session.js +6 -2
- package/dist/agent/stream.js +26 -0
- package/dist/bot/bot.js +45 -10
- package/dist/bot/command-ui.js +69 -4
- package/dist/cli/kernel-app.js +4 -2
- package/dist/dashboard/routes/accounts.js +120 -0
- package/dist/dashboard/routes/agents.js +1 -1
- package/dist/dashboard/server.js +2 -0
- package/dist/model/responses-bridge.js +37 -0
- package/package.json +1 -1
- package/packages/kernel/dist/accounts.d.ts +6 -0
- package/packages/kernel/dist/accounts.js +29 -0
- package/packages/kernel/dist/drivers/acp.d.ts +24 -0
- package/packages/kernel/dist/drivers/acp.js +477 -0
- package/packages/kernel/dist/drivers/claude.d.ts +3 -1
- package/packages/kernel/dist/drivers/claude.js +126 -9
- package/packages/kernel/dist/drivers/codex.js +35 -15
- package/packages/kernel/dist/drivers/hermes.d.ts +3 -12
- package/packages/kernel/dist/drivers/hermes.js +8 -191
- package/packages/kernel/dist/drivers/index.d.ts +1 -0
- package/packages/kernel/dist/drivers/index.js +1 -0
- package/packages/kernel/dist/index.d.ts +2 -0
- package/packages/kernel/dist/index.js +3 -0
- package/dashboard/dist/assets/AgentTab-B4ZC9QFL.js +0 -1
- package/dashboard/dist/assets/SessionPanel-BbrfUDLg.js +0 -1
- package/dashboard/dist/assets/index-A_dL4aEz.js +0 -23
- package/dashboard/dist/assets/index-Bthwt6K_.css +0 -1
- package/dashboard/dist/assets/index-D18pCeqv.js +0 -3
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { execSync, spawn } from 'node:child_process';
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
5
|
import { registerDriver } from '../driver.js';
|
|
6
|
-
import { Q, run, agentError, agentLog, agentWarn, buildStreamPreviewMeta, computeContext, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, joinErrorMessages, parseTodoWriteAsPlan, previewToolCallInput, previewToolCallResult, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, emitSessionIdUpdate, IMAGE_EXTS, mimeForExt, listPikiloomSessions, mergeManagedAndNativeSessions, managedRecordToSessionInfo, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, attachAgentImage, applyTurnWindow, shortValue, roundPercent, modelFamily, normalizeClaudeModelId, emptyUsage, normalizeUsageStatus, collapseSkillPrompt, } from '../index.js';
|
|
6
|
+
import { Q, run, agentError, agentLog, agentWarn, buildStreamPreviewMeta, computeContext, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, joinErrorMessages, parseTodoWriteAsPlan, previewToolCallInput, previewToolCallResult, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, emitSessionIdUpdate, IMAGE_EXTS, mimeForExt, listPikiloomSessions, mergeManagedAndNativeSessions, managedRecordToSessionInfo, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, attachAgentImage, applyTurnWindow, shortValue, roundPercent, toIsoFromEpochSeconds, modelFamily, normalizeClaudeModelId, emptyUsage, normalizeUsageStatus, collapseSkillPrompt, } from '../index.js';
|
|
7
7
|
import { AGENT_STREAM_HARD_KILL_GRACE_MS, AGENT_GRACEFUL_ABORT_GRACE_MS, SESSION_RUNNING_THRESHOLD_MS } from '../../core/constants.js';
|
|
8
8
|
import { terminateProcessTree } from '../../core/process-control.js';
|
|
9
9
|
import { getHome, IS_MAC, encodePathAsDirName } from '../../core/platform.js';
|
|
@@ -1775,54 +1775,187 @@ function getClaudeOAuthToken() {
|
|
|
1775
1775
|
return null;
|
|
1776
1776
|
}
|
|
1777
1777
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1778
|
+
// Shape the `/api/oauth/usage` JSON into a UsageResult (shared by the sync curl probe and the
|
|
1779
|
+
// async fetch probe). Returns null on an API error payload or when no usable window is present.
|
|
1780
|
+
function buildClaudeOAuthUsage(data) {
|
|
1781
|
+
const apiError = data?.error;
|
|
1782
|
+
if (apiError && typeof apiError === 'object')
|
|
1783
|
+
return null;
|
|
1784
|
+
const makeWindow = (label, entry) => {
|
|
1785
|
+
if (!entry || typeof entry !== 'object')
|
|
1786
|
+
return null;
|
|
1787
|
+
const usedPercent = roundPercent(entry.utilization);
|
|
1788
|
+
if (usedPercent == null)
|
|
1789
|
+
return null;
|
|
1790
|
+
const remainingPercent = Math.max(0, Math.round((100 - usedPercent) * 10) / 10);
|
|
1791
|
+
const resetAt = typeof entry.resets_at === 'string' ? entry.resets_at : null;
|
|
1792
|
+
let resetAfterSeconds = null;
|
|
1793
|
+
if (resetAt) {
|
|
1794
|
+
const resetAtMs = Date.parse(resetAt);
|
|
1795
|
+
if (Number.isFinite(resetAtMs))
|
|
1796
|
+
resetAfterSeconds = Math.max(0, Math.round((resetAtMs - Date.now()) / 1000));
|
|
1797
|
+
}
|
|
1798
|
+
return {
|
|
1799
|
+
label, usedPercent, remainingPercent, resetAt, resetAfterSeconds,
|
|
1800
|
+
status: usedPercent >= 100 ? 'limit_reached' : usedPercent >= 80 ? 'warning' : 'allowed',
|
|
1801
|
+
};
|
|
1802
|
+
};
|
|
1803
|
+
const windows = [];
|
|
1804
|
+
for (const [label, key] of [['5h', 'five_hour'], ['7d', 'seven_day'], ['7d Opus', 'seven_day_opus'], ['7d Sonnet', 'seven_day_sonnet'], ['Extra', 'extra_usage']]) {
|
|
1805
|
+
const w = makeWindow(label, data[key]);
|
|
1806
|
+
if (w)
|
|
1807
|
+
windows.push(w);
|
|
1808
|
+
}
|
|
1809
|
+
if (!windows.length)
|
|
1810
|
+
return null;
|
|
1811
|
+
const overallStatus = windows.some(w => w.status === 'limit_reached') ? 'limit_reached'
|
|
1812
|
+
: windows.some(w => w.status === 'warning') ? 'warning' : 'allowed';
|
|
1813
|
+
return { ok: true, agent: 'claude', source: 'oauth-api', capturedAt: new Date().toISOString(), status: overallStatus, windows, error: null };
|
|
1814
|
+
}
|
|
1815
|
+
function getClaudeUsageFromOAuth(tokenOverride) {
|
|
1816
|
+
const token = tokenOverride || getClaudeOAuthToken();
|
|
1780
1817
|
if (!token)
|
|
1781
1818
|
return null;
|
|
1782
1819
|
try {
|
|
1783
1820
|
const raw = execSync(`curl -s --max-time 5 -H "Authorization: Bearer ${token}" -H "anthropic-beta: oauth-2025-04-20" -H "Content-Type: application/json" "https://api.anthropic.com/api/oauth/usage"`, { encoding: 'utf-8', timeout: 8000 }).trim();
|
|
1784
1821
|
if (!raw || raw[0] !== '{')
|
|
1785
1822
|
return null;
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1823
|
+
return buildClaudeOAuthUsage(JSON.parse(raw));
|
|
1824
|
+
}
|
|
1825
|
+
catch {
|
|
1826
|
+
return null;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
// Non-blocking variant for the live (getUsageLive) path — execSync would stall the event loop.
|
|
1830
|
+
async function fetchClaudeUsageFromOAuth(tokenOverride) {
|
|
1831
|
+
const token = tokenOverride || getClaudeOAuthToken();
|
|
1832
|
+
if (!token)
|
|
1833
|
+
return null;
|
|
1834
|
+
const controller = new AbortController();
|
|
1835
|
+
const timer = setTimeout(() => controller.abort(), 5_000);
|
|
1836
|
+
try {
|
|
1837
|
+
const res = await fetch('https://api.anthropic.com/api/oauth/usage', {
|
|
1838
|
+
headers: {
|
|
1839
|
+
authorization: `Bearer ${token}`,
|
|
1840
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
1841
|
+
'content-type': 'application/json',
|
|
1842
|
+
},
|
|
1843
|
+
signal: controller.signal,
|
|
1844
|
+
});
|
|
1845
|
+
const data = await res.json().catch(() => null);
|
|
1846
|
+
return data ? buildClaudeOAuthUsage(data) : null;
|
|
1847
|
+
}
|
|
1848
|
+
catch {
|
|
1849
|
+
return null;
|
|
1850
|
+
}
|
|
1851
|
+
finally {
|
|
1852
|
+
clearTimeout(timer);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
// Per-account usage for a specific account's `claude setup-token`.
|
|
1856
|
+
//
|
|
1857
|
+
// IMPORTANT: setup-tokens are minted with the `user:inference` scope only — they do NOT carry
|
|
1858
|
+
// `user:profile`, so the read-only OAuth usage endpoint (`/api/oauth/usage`) rejects them with
|
|
1859
|
+
// `permission_error … scope requirement user:profile`. The native/default-login token works
|
|
1860
|
+
// there because it's a full login. So for account tokens we read the limit state from the
|
|
1861
|
+
// `anthropic-ratelimit-unified-*` *response headers* of a tiny inference call instead — those
|
|
1862
|
+
// headers come back on every `/v1/messages` response regardless of scope, and are exactly what
|
|
1863
|
+
// Claude Code itself uses to learn 5h / 7d limits. Cost is ~1 output token per probe, cached
|
|
1864
|
+
// for 5min per token (60s back-off on failure), with in-flight de-dup so concurrent surfaces
|
|
1865
|
+
// (cards + header + IM) share one probe.
|
|
1866
|
+
const TOKEN_USAGE_OK_TTL_MS = CLAUDE_USAGE_QUERY_TTL_MS;
|
|
1867
|
+
const TOKEN_USAGE_RETRY_TTL_MS = 60_000;
|
|
1868
|
+
const CLAUDE_USAGE_PROBE_MODEL = 'claude-haiku-4-5-20251001';
|
|
1869
|
+
const claudeTokenUsageCache = new Map();
|
|
1870
|
+
const claudeTokenUsageInflight = new Map();
|
|
1871
|
+
function usageFromRatelimitHeaders(h) {
|
|
1872
|
+
const makeWindow = (label, prefix) => {
|
|
1873
|
+
const raw = h.get(`anthropic-ratelimit-unified-${prefix}-utilization`);
|
|
1874
|
+
if (raw == null || raw === '')
|
|
1875
|
+
return null;
|
|
1876
|
+
const usedPercent = roundPercent(Number(raw) * 100);
|
|
1877
|
+
if (usedPercent == null)
|
|
1790
1878
|
return null;
|
|
1879
|
+
const remainingPercent = Math.max(0, Math.round((100 - usedPercent) * 10) / 10);
|
|
1880
|
+
const resetAt = toIsoFromEpochSeconds(h.get(`anthropic-ratelimit-unified-${prefix}-reset`));
|
|
1881
|
+
let resetAfterSeconds = null;
|
|
1882
|
+
if (resetAt) {
|
|
1883
|
+
const resetAtMs = Date.parse(resetAt);
|
|
1884
|
+
if (Number.isFinite(resetAtMs))
|
|
1885
|
+
resetAfterSeconds = Math.max(0, Math.round((resetAtMs - Date.now()) / 1000));
|
|
1791
1886
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
const usedPercent = roundPercent(entry.utilization);
|
|
1796
|
-
if (usedPercent == null)
|
|
1797
|
-
return null;
|
|
1798
|
-
const remainingPercent = Math.max(0, Math.round((100 - usedPercent) * 10) / 10);
|
|
1799
|
-
const resetAt = typeof entry.resets_at === 'string' ? entry.resets_at : null;
|
|
1800
|
-
let resetAfterSeconds = null;
|
|
1801
|
-
if (resetAt) {
|
|
1802
|
-
const resetAtMs = Date.parse(resetAt);
|
|
1803
|
-
if (Number.isFinite(resetAtMs))
|
|
1804
|
-
resetAfterSeconds = Math.max(0, Math.round((resetAtMs - Date.now()) / 1000));
|
|
1805
|
-
}
|
|
1806
|
-
return {
|
|
1807
|
-
label, usedPercent, remainingPercent, resetAt, resetAfterSeconds,
|
|
1808
|
-
status: usedPercent >= 100 ? 'limit_reached' : usedPercent >= 80 ? 'warning' : 'allowed',
|
|
1809
|
-
};
|
|
1887
|
+
return {
|
|
1888
|
+
label, usedPercent, remainingPercent, resetAt, resetAfterSeconds,
|
|
1889
|
+
status: usedPercent >= 100 ? 'limit_reached' : usedPercent >= 80 ? 'warning' : 'allowed',
|
|
1810
1890
|
};
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1891
|
+
};
|
|
1892
|
+
// Only 5h + 7d are real here. The inference headers expose an overage (Extra) on/off STATUS
|
|
1893
|
+
// but never a utilization number, and `/api/oauth/usage` (which returns the actual extra_usage
|
|
1894
|
+
// figure) is scope-blocked for setup-tokens — so we deliberately do NOT synthesize an "Extra"
|
|
1895
|
+
// window for token accounts. Showing a fabricated 0% misrepresents real Extra spend.
|
|
1896
|
+
const windows = [makeWindow('5h', '5h'), makeWindow('7d', '7d')].filter((w) => w != null);
|
|
1897
|
+
if (!windows.length)
|
|
1898
|
+
return null;
|
|
1899
|
+
const overallStatus = windows.some(w => w.status === 'limit_reached') ? 'limit_reached'
|
|
1900
|
+
: windows.some(w => w.status === 'warning') ? 'warning' : 'allowed';
|
|
1901
|
+
return { ok: true, agent: 'claude', source: 'ratelimit-headers', capturedAt: new Date().toISOString(), status: overallStatus, windows, error: null };
|
|
1902
|
+
}
|
|
1903
|
+
async function claudeUsageFromInferenceHeaders(token) {
|
|
1904
|
+
const controller = new AbortController();
|
|
1905
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
1906
|
+
try {
|
|
1907
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
1908
|
+
method: 'POST',
|
|
1909
|
+
headers: {
|
|
1910
|
+
authorization: `Bearer ${token}`,
|
|
1911
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
1912
|
+
'anthropic-version': '2023-06-01',
|
|
1913
|
+
'content-type': 'application/json',
|
|
1914
|
+
},
|
|
1915
|
+
body: JSON.stringify({ model: CLAUDE_USAGE_PROBE_MODEL, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
|
|
1916
|
+
signal: controller.signal,
|
|
1917
|
+
});
|
|
1918
|
+
// The unified rate-limit headers ride on every response (incl. 429), so parse them before
|
|
1919
|
+
// worrying about the status code. Drain the body so the socket can be reused/closed.
|
|
1920
|
+
const usage = usageFromRatelimitHeaders(res.headers);
|
|
1921
|
+
void res.text().catch(() => { });
|
|
1922
|
+
return usage;
|
|
1822
1923
|
}
|
|
1823
1924
|
catch {
|
|
1824
1925
|
return null;
|
|
1825
1926
|
}
|
|
1927
|
+
finally {
|
|
1928
|
+
clearTimeout(timer);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
export function claudeUsageForToken(token, opts) {
|
|
1932
|
+
const t = String(token || '');
|
|
1933
|
+
if (!t)
|
|
1934
|
+
return Promise.resolve(null);
|
|
1935
|
+
const now = Date.now();
|
|
1936
|
+
const cached = claudeTokenUsageCache.get(t);
|
|
1937
|
+
if (cached && !opts?.force) {
|
|
1938
|
+
const ttl = cached.ok ? TOKEN_USAGE_OK_TTL_MS : TOKEN_USAGE_RETRY_TTL_MS;
|
|
1939
|
+
if (now - cached.at < ttl)
|
|
1940
|
+
return Promise.resolve(cached.value);
|
|
1941
|
+
}
|
|
1942
|
+
const inflight = claudeTokenUsageInflight.get(t);
|
|
1943
|
+
if (inflight)
|
|
1944
|
+
return inflight;
|
|
1945
|
+
const p = (async () => {
|
|
1946
|
+
const fresh = await claudeUsageFromInferenceHeaders(t);
|
|
1947
|
+
const at = Date.now();
|
|
1948
|
+
if (fresh) {
|
|
1949
|
+
claudeTokenUsageCache.set(t, { value: fresh, at, ok: true });
|
|
1950
|
+
return fresh;
|
|
1951
|
+
}
|
|
1952
|
+
// Probe failed (network / transient): keep serving last-good, but back off retries.
|
|
1953
|
+
const value = claudeTokenUsageCache.get(t)?.value ?? null;
|
|
1954
|
+
claudeTokenUsageCache.set(t, { value, at, ok: false });
|
|
1955
|
+
return value;
|
|
1956
|
+
})().finally(() => claudeTokenUsageInflight.delete(t));
|
|
1957
|
+
claudeTokenUsageInflight.set(t, p);
|
|
1958
|
+
return p;
|
|
1826
1959
|
}
|
|
1827
1960
|
function getClaudeUsageFromTelemetry(home, model) {
|
|
1828
1961
|
const telemetryRoot = path.join(home, '.claude', 'telemetry');
|
|
@@ -2093,6 +2226,24 @@ class ClaudeDriver {
|
|
|
2093
2226
|
}
|
|
2094
2227
|
return claudeUsageCache.lastGood ?? telemetry();
|
|
2095
2228
|
}
|
|
2229
|
+
// Live usage for the agent-status surface. Unlike getUsage it ignores the 5-min query TTL and
|
|
2230
|
+
// always re-probes, so a freshly-switched login (the keychain default-login token changed) is
|
|
2231
|
+
// reflected promptly instead of serving the previous account's frozen `lastGood`. Bounded by
|
|
2232
|
+
// the caller's usage timeout, with the cached value as fallback.
|
|
2233
|
+
async getUsageLive(opts) {
|
|
2234
|
+
const home = getHome();
|
|
2235
|
+
if (!home)
|
|
2236
|
+
return emptyUsage('claude', 'HOME is not set.');
|
|
2237
|
+
const fresh = await fetchClaudeUsageFromOAuth();
|
|
2238
|
+
if (fresh) {
|
|
2239
|
+
claudeUsageCache.lastGood = fresh;
|
|
2240
|
+
claudeUsageCache.lastAttemptAt = Date.now();
|
|
2241
|
+
return fresh;
|
|
2242
|
+
}
|
|
2243
|
+
return claudeUsageCache.lastGood
|
|
2244
|
+
?? getClaudeUsageFromTelemetry(home, opts.model)
|
|
2245
|
+
?? emptyUsage('claude', 'No recent Claude usage data found.');
|
|
2246
|
+
}
|
|
2096
2247
|
async deleteNativeSession(workdir, sessionId) {
|
|
2097
2248
|
const file = claudeSessionTranscriptPath(workdir, sessionId);
|
|
2098
2249
|
if (!file || !fs.existsSync(file))
|
|
@@ -1641,6 +1641,11 @@ function getCodexSessionMessagesFromRollout(opts) {
|
|
|
1641
1641
|
const fallbackMsgs = [];
|
|
1642
1642
|
let pendingAssistant = null;
|
|
1643
1643
|
let sawAssistantResponseItems = false;
|
|
1644
|
+
// Codex records the user turn twice: a rich `response_item` (role=user, carries the image as an
|
|
1645
|
+
// input_image data URL or a localImage path) AND a text-only `event_msg`/user_message. The
|
|
1646
|
+
// user bubble is built from the latter, so stash any images from the former and attach them
|
|
1647
|
+
// when the user_message arrives — otherwise pasted images vanish from codex history on reparse.
|
|
1648
|
+
let pendingUserImages = [];
|
|
1644
1649
|
const ensureAssistant = () => {
|
|
1645
1650
|
if (!pendingAssistant)
|
|
1646
1651
|
pendingAssistant = { blocks: [], toolNamesByCallId: new Map() };
|
|
@@ -1677,12 +1682,18 @@ function getCodexSessionMessagesFromRollout(opts) {
|
|
|
1677
1682
|
if (ev.payload.type === 'user_message' && typeof ev.payload.message === 'string') {
|
|
1678
1683
|
flushAssistant();
|
|
1679
1684
|
const text = stripInjectedPrompts(ev.payload.message).trim();
|
|
1680
|
-
|
|
1685
|
+
const imageBlocks = pendingUserImages;
|
|
1686
|
+
pendingUserImages = [];
|
|
1687
|
+
if (!text && !imageBlocks.length)
|
|
1681
1688
|
continue;
|
|
1682
1689
|
const userMessage = { role: 'user', text };
|
|
1683
1690
|
fallbackMsgs.push(userMessage);
|
|
1684
1691
|
allMsgs.push(userMessage);
|
|
1685
|
-
|
|
1692
|
+
const blocks = [];
|
|
1693
|
+
if (text)
|
|
1694
|
+
blocks.push({ type: 'text', content: text });
|
|
1695
|
+
blocks.push(...imageBlocks);
|
|
1696
|
+
richMsgs.push({ role: 'user', text, blocks });
|
|
1686
1697
|
}
|
|
1687
1698
|
else if (ev.payload.type === 'agent_message' && typeof ev.payload.message === 'string') {
|
|
1688
1699
|
const text = ev.payload.message.trim();
|
|
@@ -1695,6 +1706,23 @@ function getCodexSessionMessagesFromRollout(opts) {
|
|
|
1695
1706
|
continue;
|
|
1696
1707
|
const payload = ev.payload;
|
|
1697
1708
|
if (payload.type === 'message') {
|
|
1709
|
+
if (payload.role === 'user') {
|
|
1710
|
+
for (const c of (Array.isArray(payload.content) ? payload.content : [])) {
|
|
1711
|
+
if (c?.type === 'input_image' && typeof c.image_url === 'string' && c.image_url.startsWith('data:image/')) {
|
|
1712
|
+
pendingUserImages.push({ type: 'image', content: c.image_url });
|
|
1713
|
+
}
|
|
1714
|
+
else if (c?.type === 'localImage' && typeof c.path === 'string') {
|
|
1715
|
+
try {
|
|
1716
|
+
if (fs.existsSync(c.path) && fs.statSync(c.path).size <= 4 * 1024 * 1024) {
|
|
1717
|
+
const ext = path.extname(c.path).toLowerCase();
|
|
1718
|
+
pendingUserImages.push({ type: 'image', content: `data:${mimeForExt(ext)};base64,${fs.readFileSync(c.path).toString('base64')}` });
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
catch { /* ignore */ }
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1698
1726
|
if (payload.role !== 'assistant')
|
|
1699
1727
|
continue;
|
|
1700
1728
|
const text = extractCodexMessageText(payload.content);
|
|
@@ -15,8 +15,44 @@ import { humanizeCodexError } from './drivers/codex.js';
|
|
|
15
15
|
// Tests always run legacy (the unit suite asserts legacy driver behavior). The bridge
|
|
16
16
|
// re-applies app-level parity the pure kernel must not own (claude jsonl entrypoint, codex humanize).
|
|
17
17
|
const KERNEL_AGENTS = new Set(['claude', 'codex', 'gemini', 'hermes']);
|
|
18
|
+
// Generic ACP agents the kernel can drive via @pikiloom/kernel's AcpDriver. Hermes is its
|
|
19
|
+
// own built-in preset; this registry is for ANY other ACP CLI (OpenCode, Zed, …). Extend
|
|
20
|
+
// without a code edit via PIKILOOM_ACP_AGENTS='{"zed":{"command":"zed","args":["--acp"]}}'.
|
|
21
|
+
const DEFAULT_ACP_AGENTS = {
|
|
22
|
+
opencode: { command: 'opencode', args: ['acp'] },
|
|
23
|
+
};
|
|
24
|
+
export function acpAgentConfig(agent) {
|
|
25
|
+
if (DEFAULT_ACP_AGENTS[agent])
|
|
26
|
+
return DEFAULT_ACP_AGENTS[agent];
|
|
27
|
+
try {
|
|
28
|
+
const extra = process.env.PIKILOOM_ACP_AGENTS ? JSON.parse(process.env.PIKILOOM_ACP_AGENTS) : null;
|
|
29
|
+
const c = extra?.[agent];
|
|
30
|
+
if (c && (c.command || agent))
|
|
31
|
+
return { command: String(c.command || agent), args: Array.isArray(c.args) ? c.args.map(String) : ['acp'] };
|
|
32
|
+
}
|
|
33
|
+
catch { /* malformed env → ignore */ }
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
// pikiloom's mcpServers (Record) -> kernel McpServerSpec[] for ACP session/new forwarding.
|
|
37
|
+
function mcpRecordToKernelSpecs(servers) {
|
|
38
|
+
if (!servers)
|
|
39
|
+
return [];
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
42
|
+
if (!cfg || typeof cfg !== 'object')
|
|
43
|
+
continue;
|
|
44
|
+
const type = String(cfg.type || '').toLowerCase();
|
|
45
|
+
if ((type === 'http' || type === 'sse') && cfg.url) {
|
|
46
|
+
out.push({ name, type: 'http', url: String(cfg.url), headers: cfg.headers || {} });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (cfg.command)
|
|
50
|
+
out.push({ name, type: 'stdio', command: String(cfg.command), args: Array.isArray(cfg.args) ? cfg.args.map(String) : [], env: cfg.env || {} });
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
18
54
|
export function shouldUseKernelPipeline(agent) {
|
|
19
|
-
if (!KERNEL_AGENTS.has(agent))
|
|
55
|
+
if (!KERNEL_AGENTS.has(agent) && !acpAgentConfig(agent))
|
|
20
56
|
return false;
|
|
21
57
|
if (process.env.VITEST || process.env.NODE_ENV === 'test')
|
|
22
58
|
return false; // tests assert legacy
|
|
@@ -41,7 +77,15 @@ function buildKernelDriver(kernel, opts) {
|
|
|
41
77
|
effort: opts.thinkingEffort,
|
|
42
78
|
env: opts.extraEnv,
|
|
43
79
|
mcpConfigPath: opts.mcpConfigPath ?? null,
|
|
80
|
+
// Consumed by the ACP path (Hermes + generic AcpDriver) for session/new forwarding;
|
|
81
|
+
// ignored by claude/codex/gemini (they carry MCP via config file / native registration).
|
|
82
|
+
extraMcpServers: mcpRecordToKernelSpecs(opts.mcpServers),
|
|
44
83
|
};
|
|
84
|
+
// Any registered generic ACP agent (OpenCode, …) -> the kernel's AcpDriver.
|
|
85
|
+
const acp = acpAgentConfig(opts.agent);
|
|
86
|
+
if (acp) {
|
|
87
|
+
return { driver: new kernel.AcpDriver({ id: opts.agent, command: acp.command, args: acp.args }), input: { ...common, model: opts.model ?? null } };
|
|
88
|
+
}
|
|
45
89
|
switch (opts.agent) {
|
|
46
90
|
case 'codex': {
|
|
47
91
|
// codexExtraArgs is a flattened ['-c','k=v','-c','k=v',...]; extract the k=v values
|
|
@@ -65,6 +109,22 @@ function buildKernelDriver(kernel, opts) {
|
|
|
65
109
|
};
|
|
66
110
|
}
|
|
67
111
|
}
|
|
112
|
+
// Kernel plan steps key their text as { text }; pikiloom's StreamPlan and the entire dashboard
|
|
113
|
+
// pipeline (pikichannel adapter -> ws.ts -> PlanProgressCard) key it as { step }. Translate at
|
|
114
|
+
// this seam — the same place we already remap usage fields — so codex/claude task lists render
|
|
115
|
+
// their text. Without it the progress count ("0/4") shows but every row is blank (the card reads
|
|
116
|
+
// step.step, which is undefined on the kernel shape).
|
|
117
|
+
export function toPikiloomPlan(plan) {
|
|
118
|
+
if (!plan || !Array.isArray(plan.steps))
|
|
119
|
+
return null;
|
|
120
|
+
const steps = plan.steps
|
|
121
|
+
.map((st) => ({
|
|
122
|
+
step: typeof st?.text === 'string' ? st.text : typeof st?.step === 'string' ? st.step : '',
|
|
123
|
+
status: (st?.status === 'completed' ? 'completed' : st?.status === 'inProgress' ? 'inProgress' : 'pending'),
|
|
124
|
+
}))
|
|
125
|
+
.filter((st) => st.step.trim());
|
|
126
|
+
return steps.length ? { explanation: typeof plan.explanation === 'string' ? plan.explanation : null, steps } : null;
|
|
127
|
+
}
|
|
68
128
|
let _kernel = null;
|
|
69
129
|
export async function loadKernel() {
|
|
70
130
|
if (_kernel)
|
|
@@ -141,7 +201,7 @@ export async function kernelStream(opts) {
|
|
|
141
201
|
providerName: opts.byokProviderName ?? null,
|
|
142
202
|
};
|
|
143
203
|
try {
|
|
144
|
-
opts.onText(s.text || '', s.reasoning || '', s.activity || '', m, s.plan
|
|
204
|
+
opts.onText(s.text || '', s.reasoning || '', s.activity || '', m, toPikiloomPlan(s.plan));
|
|
145
205
|
}
|
|
146
206
|
catch { /* isolate */ }
|
|
147
207
|
};
|
|
@@ -185,7 +245,7 @@ export async function kernelStream(opts) {
|
|
|
185
245
|
ok: !!result.ok,
|
|
186
246
|
message: (result.text || snapshot.text || '').trim() || (finalError ?? '(no output)'),
|
|
187
247
|
thinking: (result.reasoning || snapshot.reasoning || '').trim() || null,
|
|
188
|
-
plan: snapshot.plan
|
|
248
|
+
plan: toPikiloomPlan(snapshot.plan),
|
|
189
249
|
sessionId: finalSessionId,
|
|
190
250
|
workspacePath: null,
|
|
191
251
|
model: input.model ?? null,
|
package/dist/agent/session.js
CHANGED
|
@@ -184,6 +184,7 @@ function normalizeSessionRecord(raw, workdir) {
|
|
|
184
184
|
thinkingEffort: typeof raw?.thinkingEffort === 'string' && raw.thinkingEffort.trim() ? raw.thinkingEffort.trim() : null,
|
|
185
185
|
workflowEnabled: typeof raw?.workflowEnabled === 'boolean' ? raw.workflowEnabled : null,
|
|
186
186
|
profileId: typeof raw?.profileId === 'string' && raw.profileId.trim() ? raw.profileId.trim() : null,
|
|
187
|
+
accountId: typeof raw?.accountId === 'string' && raw.accountId.trim() ? raw.accountId.trim() : null,
|
|
187
188
|
stagedFiles: Array.isArray(raw?.stagedFiles) ? dedupeStrings(raw.stagedFiles.filter((v) => typeof v === 'string')) : [],
|
|
188
189
|
lastUserAttachments: Array.isArray(raw?.lastUserAttachments)
|
|
189
190
|
? dedupeStrings(raw.lastUserAttachments.filter((v) => typeof v === 'string'))
|
|
@@ -327,7 +328,7 @@ function writeSessionMeta(record) {
|
|
|
327
328
|
workspacePath: record.workspacePath,
|
|
328
329
|
threadId: record.threadId,
|
|
329
330
|
createdAt: record.createdAt, updatedAt: record.updatedAt,
|
|
330
|
-
title: record.title, model: record.model, thinkingEffort: record.thinkingEffort, workflowEnabled: record.workflowEnabled, profileId: record.profileId, stagedFiles: record.stagedFiles,
|
|
331
|
+
title: record.title, model: record.model, thinkingEffort: record.thinkingEffort, workflowEnabled: record.workflowEnabled, profileId: record.profileId, accountId: record.accountId ?? null, stagedFiles: record.stagedFiles,
|
|
331
332
|
runState: record.runState, runDetail: record.runDetail, runUpdatedAt: record.runUpdatedAt,
|
|
332
333
|
runPid: record.runPid,
|
|
333
334
|
classification: record.classification,
|
|
@@ -563,7 +564,7 @@ export function ensureSessionWorkspace(opts) {
|
|
|
563
564
|
workspacePath: sessionWorkspacePath(workdir, opts.agent, sessionId),
|
|
564
565
|
threadId,
|
|
565
566
|
createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
|
|
566
|
-
title: summarizePromptTitle(opts.title) || null, model: null, thinkingEffort: null, workflowEnabled: null, profileId: null, stagedFiles: [], lastUserAttachments: [],
|
|
567
|
+
title: summarizePromptTitle(opts.title) || null, model: null, thinkingEffort: null, workflowEnabled: null, profileId: null, accountId: null, stagedFiles: [], lastUserAttachments: [],
|
|
567
568
|
runState: 'completed', runDetail: null, runUpdatedAt: new Date().toISOString(),
|
|
568
569
|
runPid: null,
|
|
569
570
|
classification: null, userStatus: null, userNote: null,
|
|
@@ -597,6 +598,7 @@ export function managedRecordToSessionInfo(record) {
|
|
|
597
598
|
thinkingEffort: record.thinkingEffort,
|
|
598
599
|
workflowEnabled: record.workflowEnabled ?? null,
|
|
599
600
|
profileId: record.profileId ?? null,
|
|
601
|
+
accountId: record.accountId ?? null,
|
|
600
602
|
createdAt: record.createdAt,
|
|
601
603
|
title,
|
|
602
604
|
running: record.runState === 'running',
|
|
@@ -680,6 +682,7 @@ export function getSessionStoredConfig(workdir, agent, sessionId) {
|
|
|
680
682
|
thinkingEffort: record?.thinkingEffort ?? null,
|
|
681
683
|
workflowEnabled: record?.workflowEnabled ?? null,
|
|
682
684
|
profileId: record?.profileId ?? null,
|
|
685
|
+
accountId: record?.accountId ?? null,
|
|
683
686
|
};
|
|
684
687
|
}
|
|
685
688
|
export function ensureManagedSession(opts) {
|
|
@@ -774,6 +777,7 @@ export function mergeManagedAndNativeSessions(managedSessions, nativeSessions) {
|
|
|
774
777
|
thinkingEffort: managed.thinkingEffort ?? native.thinkingEffort ?? null,
|
|
775
778
|
workflowEnabled: managed.workflowEnabled ?? native.workflowEnabled ?? null,
|
|
776
779
|
profileId: managed.profileId ?? native.profileId ?? null,
|
|
780
|
+
accountId: managed.accountId ?? native.accountId ?? null,
|
|
777
781
|
createdAt: native.createdAt || managed.createdAt,
|
|
778
782
|
classification: managed.classification ?? native.classification ?? null,
|
|
779
783
|
userStatus: managed.userStatus ?? native.userStatus ?? null,
|
package/dist/agent/stream.js
CHANGED
|
@@ -13,6 +13,7 @@ import { saveSessionRecord, setSessionRunState, applySessionRunResult, ensureSes
|
|
|
13
13
|
import { clearAwaitResume } from './await-resume.js';
|
|
14
14
|
import { collapseSkillPrompt } from './skills.js';
|
|
15
15
|
import { shouldUseKernelPipeline, kernelStream } from './kernel-bridge.js';
|
|
16
|
+
import { accountAgentSupported, resolveAccountEnv, updateAccount } from './accounts.js';
|
|
16
17
|
function trimSessionText(value, max = 24_000) {
|
|
17
18
|
const text = typeof value === 'string' ? value.trim() : '';
|
|
18
19
|
if (!text)
|
|
@@ -531,6 +532,29 @@ export async function doStream(opts) {
|
|
|
531
532
|
catch (e) {
|
|
532
533
|
agentWarn(`[byok] failed to apply Profile injection: ${e?.message || e}`);
|
|
533
534
|
}
|
|
535
|
+
// Multi-account (native subscription): when NO BYOK profile is bound, route the turn
|
|
536
|
+
// through the agent's active local account by injecting its isolated config dir
|
|
537
|
+
// (CLAUDE_CONFIG_DIR / CODEX_HOME). A BYOK profile owns the credential, so it takes over
|
|
538
|
+
// and account injection is skipped — the two axes are mutually exclusive per turn.
|
|
539
|
+
try {
|
|
540
|
+
if (accountAgentSupported(prepared.agent)) {
|
|
541
|
+
const profileBound = prepared.profileId === undefined
|
|
542
|
+
? !!getActiveProfile(prepared.agent)
|
|
543
|
+
: !!(prepared.profileId && getProfile(prepared.profileId));
|
|
544
|
+
if (!profileBound) {
|
|
545
|
+
const resolved = await resolveAccountEnv(prepared.agent, prepared.accountId);
|
|
546
|
+
if (resolved) {
|
|
547
|
+
prepared.extraEnv = { ...(prepared.extraEnv || {}), ...resolved.env };
|
|
548
|
+
prepared.accountId = resolved.accountId;
|
|
549
|
+
agentLog(`[account] ${prepared.agent} turn → account=${resolved.accountId} (${Object.keys(resolved.env).join('+')})`);
|
|
550
|
+
void updateAccount(prepared.agent, resolved.accountId, { lastUsedAt: new Date().toISOString() }).catch(() => { });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
catch (e) {
|
|
556
|
+
agentWarn(`[account] injection failed: ${e?.message || e}`);
|
|
557
|
+
}
|
|
534
558
|
try {
|
|
535
559
|
if (prepared.thinkingEffort) {
|
|
536
560
|
session.record.thinkingEffort = prepared.thinkingEffort.trim().toLowerCase() || session.record.thinkingEffort;
|
|
@@ -548,6 +572,8 @@ export async function doStream(opts) {
|
|
|
548
572
|
session.record.model = turnModel;
|
|
549
573
|
if (prepared.profileId !== undefined)
|
|
550
574
|
session.record.profileId = prepared.profileId || null;
|
|
575
|
+
if (prepared.accountId !== undefined)
|
|
576
|
+
session.record.accountId = prepared.accountId || null;
|
|
551
577
|
saveSessionRecord(opts.workdir, session.record);
|
|
552
578
|
}
|
|
553
579
|
catch (e) {
|
package/dist/bot/bot.js
CHANGED
|
@@ -2,7 +2,7 @@ import os from 'node:os';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { execSync, spawn } from 'node:child_process';
|
|
4
4
|
import { getActiveUserConfig, loadWorkspaces, onUserConfigChange, resolveUserWorkdir, setUserWorkdir, updateUserConfig } from '../core/config/user-config.js';
|
|
5
|
-
import { doStream, ensureManagedSession, findManagedThreadSession, getSessionStoredConfig, getUsage, initializeProjectSkills, listAgents, resolveAgentModels, resolveDefaultAgent, listSkills, stageSessionFiles, reconcileOrphanedRunningSessions, getAgentBoundModelId, setAgentBoundModelId, collapseSkillPrompt, readGoal, accountTurn, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, bumpContinuationCount, pauseGoal, resumeGoal, setGoal as setGoalState, clearGoal as clearGoalState, setCodexGoal, getCodexGoal, clearCodexGoal, pauseCodexGoal, resumeCodexGoal, getClaudeNativeGoal, buildClaudeSetGoalPrompt, buildClaudeClearGoalPrompt, deliverArtifact, attachmentUrl, isPendingSessionId, } from '../agent/index.js';
|
|
5
|
+
import { doStream, ensureManagedSession, findManagedThreadSession, getSessionStoredConfig, getUsage, initializeProjectSkills, listAgents, resolveAgentModels, resolveDefaultAgent, listSkills, stageSessionFiles, reconcileOrphanedRunningSessions, getAgentBoundModelId, setAgentBoundModelId, collapseSkillPrompt, readGoal, accountTurn, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, bumpContinuationCount, pauseGoal, resumeGoal, setGoal as setGoalState, clearGoal as clearGoalState, setCodexGoal, getCodexGoal, clearCodexGoal, pauseCodexGoal, resumeCodexGoal, getClaudeNativeGoal, buildClaudeSetGoalPrompt, buildClaudeClearGoalPrompt, deliverArtifact, attachmentUrl, attachAgentImage, rewriteAttachmentBlocksForTransport, isPendingSessionId, } from '../agent/index.js';
|
|
6
6
|
import { compactForHandover, describeHandoverRef } from '../agent/handover.js';
|
|
7
7
|
import { getActiveProfileId, getActiveProfile, setActiveProfile } from '../model/index.js';
|
|
8
8
|
import { querySessions, querySessionTail, updateSession, } from './session-hub.js';
|
|
@@ -160,16 +160,48 @@ export class Bot {
|
|
|
160
160
|
const snap = this.streamSnapshots.get(this.resolveSessionKey(sessionKey));
|
|
161
161
|
return snap ? this.enrichSnapshot(snap) : null;
|
|
162
162
|
}
|
|
163
|
+
taskAttachmentBlocks(task) {
|
|
164
|
+
if (!task?.attachments?.length)
|
|
165
|
+
return [];
|
|
166
|
+
const session = this.getSessionRuntimeByKey(task.sessionKey, { allowAnyWorkdir: true });
|
|
167
|
+
const sessionId = session?.sessionId || task.sessionKey.slice(task.agent.length + 1);
|
|
168
|
+
if (!sessionId)
|
|
169
|
+
return [];
|
|
170
|
+
const blocks = task.attachments
|
|
171
|
+
.map(imagePath => attachAgentImage({ imagePath, inlineThresholdBytes: 0 }))
|
|
172
|
+
.filter((block) => !!block);
|
|
173
|
+
return blocks.length
|
|
174
|
+
? rewriteAttachmentBlocksForTransport(blocks, { agent: task.agent, sessionId })
|
|
175
|
+
: [];
|
|
176
|
+
}
|
|
177
|
+
taskPreview(taskId) {
|
|
178
|
+
const task = this.activeTasks.get(taskId) || null;
|
|
179
|
+
const raw = task?.prompt || '';
|
|
180
|
+
const attachments = this.taskAttachmentBlocks(task);
|
|
181
|
+
return {
|
|
182
|
+
taskId,
|
|
183
|
+
prompt: collapseSkillPrompt(raw) ?? raw,
|
|
184
|
+
...(attachments.length ? { attachments } : {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
163
187
|
enrichSnapshot(snap) {
|
|
164
188
|
let next = snap;
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
189
|
+
const runningTask = next.taskId ? this.activeTasks.get(next.taskId) : null;
|
|
190
|
+
const runningPrompt = runningTask?.prompt || '';
|
|
191
|
+
if (runningPrompt) {
|
|
192
|
+
const questionBlocks = this.taskAttachmentBlocks(runningTask);
|
|
193
|
+
next = {
|
|
194
|
+
...next,
|
|
195
|
+
question: collapseSkillPrompt(runningPrompt) ?? runningPrompt,
|
|
196
|
+
...(questionBlocks.length ? { questionBlocks } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const queuedIds = [
|
|
200
|
+
...(next.phase === 'queued' && next.taskId ? [next.taskId] : []),
|
|
201
|
+
...(next.queuedTaskIds || []),
|
|
202
|
+
].filter((taskId, index, all) => taskId && all.indexOf(taskId) === index);
|
|
203
|
+
if (queuedIds.length) {
|
|
204
|
+
const queuedTasks = queuedIds.map(taskId => this.taskPreview(taskId));
|
|
173
205
|
next = { ...next, queuedTasks };
|
|
174
206
|
}
|
|
175
207
|
if (next.interactions?.length) {
|
|
@@ -234,13 +266,16 @@ export class Bot {
|
|
|
234
266
|
switch (event.type) {
|
|
235
267
|
case 'queued': {
|
|
236
268
|
const existing = this.streamSnapshots.get(sessionKey);
|
|
237
|
-
if (existing &&
|
|
269
|
+
if (existing && existing.phase === 'streaming') {
|
|
238
270
|
const list = existing.queuedTaskIds ? [...existing.queuedTaskIds] : [];
|
|
239
271
|
if (existing.taskId !== event.taskId && !list.includes(event.taskId))
|
|
240
272
|
list.push(event.taskId);
|
|
241
273
|
existing.queuedTaskIds = list.length ? list : undefined;
|
|
242
274
|
existing.updatedAt = now;
|
|
243
275
|
}
|
|
276
|
+
else if (existing && existing.phase === 'done') {
|
|
277
|
+
this.streamSnapshots.set(sessionKey, { phase: 'queued', taskId: event.taskId, updatedAt: now });
|
|
278
|
+
}
|
|
244
279
|
else if (existing && existing.phase === 'queued') {
|
|
245
280
|
const list = existing.queuedTaskIds ? [...existing.queuedTaskIds] : [];
|
|
246
281
|
if (existing.taskId !== event.taskId && !list.includes(event.taskId))
|