pikiloom 0.4.40 → 0.4.42
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-B9z4W2mh.js +1 -0
- package/dashboard/dist/assets/{ConnectionModal-g_6IHAWE.js → ConnectionModal-2bL7sFKk.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-DVe6q12N.js → DirBrowser-CA4k8E_A.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-GqxD61Pp.js → ExtensionsTab-CRjTmFRl.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-DIqTzZJ9.js → IMAccessTab-DDb7rsHe.js} +1 -1
- package/dashboard/dist/assets/{Modal-CIYrMwm0.js → Modal-DDiBG4kb.js} +1 -1
- package/dashboard/dist/assets/{Modals-B-Afu2rD.js → Modals-CsUQjpxn.js} +1 -1
- package/dashboard/dist/assets/{Select-B1UL27gw.js → Select-CG_7h_Tz.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-B5964GUj.js +1 -0
- package/dashboard/dist/assets/{SystemTab-Bv1uT5-h.js → SystemTab-D7r1PsC9.js} +1 -1
- package/dashboard/dist/assets/index-BbplxgnQ.js +3 -0
- package/dashboard/dist/assets/index-CULTTvtY.js +23 -0
- package/dashboard/dist/assets/index-DwmXPtDd.css +1 -0
- package/dashboard/dist/assets/{shared-624PaUfm.js → shared-Bv8WvQSo.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/accounts.js +181 -0
- package/dist/agent/drivers/claude.js +108 -3
- package/dist/agent/kernel-bridge.js +45 -1
- 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/bot/render-shared.js +2 -1
- package/dist/cli/kernel-app.js +4 -2
- package/dist/core/config/runtime-config.js +14 -22
- package/dist/dashboard/routes/accounts.js +113 -0
- package/dist/dashboard/server.js +2 -0
- package/dist/model/responses-bridge.js +37 -0
- package/package.json +1 -1
- package/packages/kernel/README.md +35 -0
- package/packages/kernel/dist/accounts.d.ts +6 -0
- package/packages/kernel/dist/accounts.js +29 -0
- package/packages/kernel/dist/contracts/driver.d.ts +15 -0
- package/packages/kernel/dist/contracts/ports.d.ts +2 -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 +5 -1
- package/packages/kernel/dist/drivers/claude.js +4 -0
- package/packages/kernel/dist/drivers/codex.d.ts +5 -1
- package/packages/kernel/dist/drivers/codex.js +24 -12
- package/packages/kernel/dist/drivers/gemini.d.ts +5 -1
- package/packages/kernel/dist/drivers/gemini.js +4 -0
- 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 +4 -1
- package/packages/kernel/dist/index.js +5 -0
- package/packages/kernel/dist/ports/defaults.js +7 -1
- package/packages/kernel/dist/runtime/loom.d.ts +10 -0
- package/packages/kernel/dist/runtime/loom.js +18 -2
- package/packages/kernel/dist/workspace/index.d.ts +5 -0
- package/packages/kernel/dist/workspace/index.js +7 -0
- package/packages/kernel/dist/workspace/mcp.d.ts +44 -0
- package/packages/kernel/dist/workspace/mcp.js +82 -0
- package/packages/kernel/dist/workspace/native.d.ts +14 -0
- package/packages/kernel/dist/workspace/native.js +340 -0
- package/packages/kernel/dist/workspace/paths.d.ts +33 -0
- package/packages/kernel/dist/workspace/paths.js +40 -0
- package/packages/kernel/dist/workspace/sessions.d.ts +49 -0
- package/packages/kernel/dist/workspace/sessions.js +130 -0
- package/packages/kernel/dist/workspace/skills.d.ts +44 -0
- package/packages/kernel/dist/workspace/skills.js +173 -0
- package/dashboard/dist/assets/AgentTab-Cj7sekcS.js +0 -1
- package/dashboard/dist/assets/SessionPanel-BHuG5hxj.js +0 -1
- package/dashboard/dist/assets/index-BOEW0RIx.js +0 -23
- package/dashboard/dist/assets/index-Bthwt6K_.css +0 -1
- package/dashboard/dist/assets/index-qKexKdJl.js +0 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as s}from"./react-vendor-C7Sl8SE7.js";import{
|
|
1
|
+
import{j as s}from"./react-vendor-C7Sl8SE7.js";import{O as t,m as i,S as a}from"./index-BbplxgnQ.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-BbplxgnQ.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-DwmXPtDd.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Multi-account store (app layer): local subscription accounts for an agent, switched by
|
|
2
|
+
// injecting a long-lived auth token at spawn. Each account = a user-named token (minted via
|
|
3
|
+
// `claude setup-token`), stored in the credential vault (not plaintext). The agent keeps
|
|
4
|
+
// using its normal single config home; only CLAUDE_CODE_OAUTH_TOKEN changes.
|
|
5
|
+
//
|
|
6
|
+
// claude only for now — codex has no token-based switch (see kernel accounts.ts).
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { loadUserConfig, saveUserConfig } from '../core/config/user-config.js';
|
|
9
|
+
import { persistSecret, forgetSecret, resolveCredential, isCredentialRef } from '../core/secrets/index.js';
|
|
10
|
+
import { loadKernel } from './kernel-bridge.js';
|
|
11
|
+
import { claudeUsageForToken } from './drivers/claude.js';
|
|
12
|
+
import { agentWarn } from './utils.js';
|
|
13
|
+
// Agents whose local accounts are switched by a token (kept in sync with the kernel).
|
|
14
|
+
const ACCOUNT_AGENTS = new Set(['claude']);
|
|
15
|
+
export const MAX_ACCOUNTS_PER_AGENT = 5;
|
|
16
|
+
const TOKEN_RE = /^sk-ant-oat/;
|
|
17
|
+
export function accountAgentSupported(agent) {
|
|
18
|
+
return ACCOUNT_AGENTS.has(agent);
|
|
19
|
+
}
|
|
20
|
+
function getLayer() {
|
|
21
|
+
return loadUserConfig().accounts || {};
|
|
22
|
+
}
|
|
23
|
+
function writeLayer(layer) {
|
|
24
|
+
saveUserConfig({ ...loadUserConfig(), accounts: layer });
|
|
25
|
+
}
|
|
26
|
+
// Only records that carry a resolvable credential are real accounts; this also drops stale
|
|
27
|
+
// records from the earlier config-directory implementation (which had no credential).
|
|
28
|
+
function validRecords(agent) {
|
|
29
|
+
return (getLayer().byAgent?.[agent] || []).filter(r => r && isCredentialRef(r.credential));
|
|
30
|
+
}
|
|
31
|
+
export function listAccounts(agent) {
|
|
32
|
+
return validRecords(agent).slice().sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
33
|
+
}
|
|
34
|
+
export function getAccount(agent, id) {
|
|
35
|
+
return validRecords(agent).find(a => a.id === id) || null;
|
|
36
|
+
}
|
|
37
|
+
export async function addAccount(agent, label, token) {
|
|
38
|
+
if (!accountAgentSupported(agent))
|
|
39
|
+
throw new Error(`Agent ${agent} does not support multiple accounts`);
|
|
40
|
+
const name = (label || '').trim();
|
|
41
|
+
if (!name)
|
|
42
|
+
throw new Error('Account name is required');
|
|
43
|
+
const t = (token || '').trim();
|
|
44
|
+
if (!TOKEN_RE.test(t))
|
|
45
|
+
throw new Error('Expected a token starting with "sk-ant-oat" (the output of `claude setup-token`)');
|
|
46
|
+
const existing = listAccounts(agent);
|
|
47
|
+
if (existing.length >= MAX_ACCOUNTS_PER_AGENT)
|
|
48
|
+
throw new Error(`At most ${MAX_ACCOUNTS_PER_AGENT} accounts per agent`);
|
|
49
|
+
const id = randomUUID().slice(0, 8);
|
|
50
|
+
const credential = await persistSecret(`account/${agent}/${id}`, t);
|
|
51
|
+
const rec = { id, label: name, credential, createdAt: new Date().toISOString(), lastUsedAt: null };
|
|
52
|
+
const layer = getLayer();
|
|
53
|
+
const byAgent = { ...(layer.byAgent || {}) };
|
|
54
|
+
byAgent[agent] = [...existing, rec];
|
|
55
|
+
writeLayer({ ...layer, byAgent });
|
|
56
|
+
return rec;
|
|
57
|
+
}
|
|
58
|
+
export async function updateAccount(agent, id, patch) {
|
|
59
|
+
const list = listAccounts(agent);
|
|
60
|
+
const idx = list.findIndex(a => a.id === id);
|
|
61
|
+
if (idx < 0)
|
|
62
|
+
throw new Error(`Account not found: ${id}`);
|
|
63
|
+
const cur = list[idx];
|
|
64
|
+
let credential = cur.credential;
|
|
65
|
+
if (patch.token !== undefined) {
|
|
66
|
+
const t = patch.token.trim();
|
|
67
|
+
if (!TOKEN_RE.test(t))
|
|
68
|
+
throw new Error('Expected a token starting with "sk-ant-oat"');
|
|
69
|
+
credential = await persistSecret(`account/${agent}/${id}`, t);
|
|
70
|
+
}
|
|
71
|
+
const next = {
|
|
72
|
+
...cur,
|
|
73
|
+
credential,
|
|
74
|
+
...('label' in patch && patch.label !== undefined ? { label: patch.label.trim() || cur.label } : {}),
|
|
75
|
+
...('lastUsedAt' in patch ? { lastUsedAt: patch.lastUsedAt ?? null } : {}),
|
|
76
|
+
};
|
|
77
|
+
list[idx] = next;
|
|
78
|
+
const layer = getLayer();
|
|
79
|
+
writeLayer({ ...layer, byAgent: { ...(layer.byAgent || {}), [agent]: list } });
|
|
80
|
+
return next;
|
|
81
|
+
}
|
|
82
|
+
export async function removeAccount(agent, id) {
|
|
83
|
+
const rec = getAccount(agent, id);
|
|
84
|
+
if (!rec)
|
|
85
|
+
return false;
|
|
86
|
+
const layer = getLayer();
|
|
87
|
+
const byAgent = { ...(layer.byAgent || {}) };
|
|
88
|
+
byAgent[agent] = listAccounts(agent).filter(a => a.id !== id);
|
|
89
|
+
const activeByAgent = { ...(layer.activeByAgent || {}) };
|
|
90
|
+
if (activeByAgent[agent] === id)
|
|
91
|
+
activeByAgent[agent] = null;
|
|
92
|
+
writeLayer({ ...layer, byAgent, activeByAgent });
|
|
93
|
+
try {
|
|
94
|
+
await forgetSecret(rec.credential);
|
|
95
|
+
}
|
|
96
|
+
catch { /* best-effort */ }
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
export function getActiveAccountId(agent) {
|
|
100
|
+
const id = getLayer().activeByAgent?.[agent] || null;
|
|
101
|
+
if (id && !getAccount(agent, id))
|
|
102
|
+
return null;
|
|
103
|
+
return id;
|
|
104
|
+
}
|
|
105
|
+
export function setActiveAccount(agent, id) {
|
|
106
|
+
if (id && !getAccount(agent, id))
|
|
107
|
+
throw new Error(`Account not found: ${id}`);
|
|
108
|
+
const layer = getLayer();
|
|
109
|
+
writeLayer({ ...layer, activeByAgent: { ...(layer.activeByAgent || {}), [agent]: id } });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolve the env to inject for a turn. Tri-state override (mirrors profileId):
|
|
113
|
+
* undefined -> use the agent's globally-active account
|
|
114
|
+
* null -> no account (default login)
|
|
115
|
+
* <id> -> that specific account
|
|
116
|
+
*/
|
|
117
|
+
export async function resolveAccountEnv(agent, accountIdOverride) {
|
|
118
|
+
if (!accountAgentSupported(agent))
|
|
119
|
+
return null;
|
|
120
|
+
const id = accountIdOverride === undefined ? getActiveAccountId(agent) : accountIdOverride;
|
|
121
|
+
if (!id)
|
|
122
|
+
return null;
|
|
123
|
+
const rec = getAccount(agent, id);
|
|
124
|
+
if (!rec)
|
|
125
|
+
return null;
|
|
126
|
+
let token = '';
|
|
127
|
+
try {
|
|
128
|
+
token = await resolveCredential(rec.credential);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
agentWarn(`[account] could not resolve token for ${agent}/${id}: ${e?.message || e}`);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
if (!token)
|
|
135
|
+
return null;
|
|
136
|
+
const kernel = await loadKernel();
|
|
137
|
+
const env = kernel.accountTokenEnv(agent, token);
|
|
138
|
+
return { env, accountId: id };
|
|
139
|
+
}
|
|
140
|
+
// Last-known per-account usage, kept so synchronous surfaces (the IM `/agents` view builder)
|
|
141
|
+
// can render usage without awaiting a probe. Filled whenever `probeAccountUsage` resolves.
|
|
142
|
+
const accountUsageById = new Map();
|
|
143
|
+
const usageKey = (agent, id) => `${agent}/${id}`;
|
|
144
|
+
/** Best-effort per-account usage: read the account's quota from its token (see claudeUsageForToken). */
|
|
145
|
+
export async function probeAccountUsage(agent, id) {
|
|
146
|
+
if (agent !== 'claude')
|
|
147
|
+
return null;
|
|
148
|
+
const rec = getAccount(agent, id);
|
|
149
|
+
if (!rec)
|
|
150
|
+
return null;
|
|
151
|
+
try {
|
|
152
|
+
const token = await resolveCredential(rec.credential);
|
|
153
|
+
const usage = token ? await claudeUsageForToken(token) : null;
|
|
154
|
+
accountUsageById.set(usageKey(agent, id), usage);
|
|
155
|
+
return usage;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Synchronous read of the last-probed usage for an account (no network), or null. */
|
|
162
|
+
export function getCachedAccountUsage(agent, id) {
|
|
163
|
+
return accountUsageById.get(usageKey(agent, id)) ?? null;
|
|
164
|
+
}
|
|
165
|
+
/** Fire-and-forget refresh of every account's usage (cached + de-duped under the hood). */
|
|
166
|
+
export function warmAccountUsages(agent) {
|
|
167
|
+
if (!accountAgentSupported(agent))
|
|
168
|
+
return;
|
|
169
|
+
for (const rec of listAccounts(agent))
|
|
170
|
+
void probeAccountUsage(agent, rec.id);
|
|
171
|
+
}
|
|
172
|
+
/** Compact "5h 100% · 7d 19%" summary from the cached usage, or null. */
|
|
173
|
+
export function accountUsageSummary(agent, id) {
|
|
174
|
+
const usage = getCachedAccountUsage(agent, id);
|
|
175
|
+
if (!usage?.ok || !usage.windows.length)
|
|
176
|
+
return null;
|
|
177
|
+
const parts = usage.windows
|
|
178
|
+
.filter(w => w.usedPercent != null)
|
|
179
|
+
.map(w => `${w.label} ${Math.round(w.usedPercent)}%`);
|
|
180
|
+
return parts.length ? parts.join(' · ') : null;
|
|
181
|
+
}
|
|
@@ -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,8 +1775,8 @@ function getClaudeOAuthToken() {
|
|
|
1775
1775
|
return null;
|
|
1776
1776
|
}
|
|
1777
1777
|
}
|
|
1778
|
-
function getClaudeUsageFromOAuth() {
|
|
1779
|
-
const token = getClaudeOAuthToken();
|
|
1778
|
+
function getClaudeUsageFromOAuth(tokenOverride) {
|
|
1779
|
+
const token = tokenOverride || getClaudeOAuthToken();
|
|
1780
1780
|
if (!token)
|
|
1781
1781
|
return null;
|
|
1782
1782
|
try {
|
|
@@ -1824,6 +1824,111 @@ function getClaudeUsageFromOAuth() {
|
|
|
1824
1824
|
return null;
|
|
1825
1825
|
}
|
|
1826
1826
|
}
|
|
1827
|
+
// Per-account usage for a specific account's `claude setup-token`.
|
|
1828
|
+
//
|
|
1829
|
+
// IMPORTANT: setup-tokens are minted with the `user:inference` scope only — they do NOT carry
|
|
1830
|
+
// `user:profile`, so the read-only OAuth usage endpoint (`/api/oauth/usage`) rejects them with
|
|
1831
|
+
// `permission_error … scope requirement user:profile`. The native/default-login token works
|
|
1832
|
+
// there because it's a full login. So for account tokens we read the limit state from the
|
|
1833
|
+
// `anthropic-ratelimit-unified-*` *response headers* of a tiny inference call instead — those
|
|
1834
|
+
// headers come back on every `/v1/messages` response regardless of scope, and are exactly what
|
|
1835
|
+
// Claude Code itself uses to learn 5h / 7d limits. Cost is ~1 output token per probe, cached
|
|
1836
|
+
// for 5min per token (60s back-off on failure), with in-flight de-dup so concurrent surfaces
|
|
1837
|
+
// (cards + header + IM) share one probe.
|
|
1838
|
+
const TOKEN_USAGE_OK_TTL_MS = CLAUDE_USAGE_QUERY_TTL_MS;
|
|
1839
|
+
const TOKEN_USAGE_RETRY_TTL_MS = 60_000;
|
|
1840
|
+
const CLAUDE_USAGE_PROBE_MODEL = 'claude-haiku-4-5-20251001';
|
|
1841
|
+
const claudeTokenUsageCache = new Map();
|
|
1842
|
+
const claudeTokenUsageInflight = new Map();
|
|
1843
|
+
function usageFromRatelimitHeaders(h) {
|
|
1844
|
+
const makeWindow = (label, prefix) => {
|
|
1845
|
+
const raw = h.get(`anthropic-ratelimit-unified-${prefix}-utilization`);
|
|
1846
|
+
if (raw == null || raw === '')
|
|
1847
|
+
return null;
|
|
1848
|
+
const usedPercent = roundPercent(Number(raw) * 100);
|
|
1849
|
+
if (usedPercent == null)
|
|
1850
|
+
return null;
|
|
1851
|
+
const remainingPercent = Math.max(0, Math.round((100 - usedPercent) * 10) / 10);
|
|
1852
|
+
const resetAt = toIsoFromEpochSeconds(h.get(`anthropic-ratelimit-unified-${prefix}-reset`));
|
|
1853
|
+
let resetAfterSeconds = null;
|
|
1854
|
+
if (resetAt) {
|
|
1855
|
+
const resetAtMs = Date.parse(resetAt);
|
|
1856
|
+
if (Number.isFinite(resetAtMs))
|
|
1857
|
+
resetAfterSeconds = Math.max(0, Math.round((resetAtMs - Date.now()) / 1000));
|
|
1858
|
+
}
|
|
1859
|
+
return {
|
|
1860
|
+
label, usedPercent, remainingPercent, resetAt, resetAfterSeconds,
|
|
1861
|
+
status: usedPercent >= 100 ? 'limit_reached' : usedPercent >= 80 ? 'warning' : 'allowed',
|
|
1862
|
+
};
|
|
1863
|
+
};
|
|
1864
|
+
// Only 5h + 7d are real here. The inference headers expose an overage (Extra) on/off STATUS
|
|
1865
|
+
// but never a utilization number, and `/api/oauth/usage` (which returns the actual extra_usage
|
|
1866
|
+
// figure) is scope-blocked for setup-tokens — so we deliberately do NOT synthesize an "Extra"
|
|
1867
|
+
// window for token accounts. Showing a fabricated 0% misrepresents real Extra spend.
|
|
1868
|
+
const windows = [makeWindow('5h', '5h'), makeWindow('7d', '7d')].filter((w) => w != null);
|
|
1869
|
+
if (!windows.length)
|
|
1870
|
+
return null;
|
|
1871
|
+
const overallStatus = windows.some(w => w.status === 'limit_reached') ? 'limit_reached'
|
|
1872
|
+
: windows.some(w => w.status === 'warning') ? 'warning' : 'allowed';
|
|
1873
|
+
return { ok: true, agent: 'claude', source: 'ratelimit-headers', capturedAt: new Date().toISOString(), status: overallStatus, windows, error: null };
|
|
1874
|
+
}
|
|
1875
|
+
async function claudeUsageFromInferenceHeaders(token) {
|
|
1876
|
+
const controller = new AbortController();
|
|
1877
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
1878
|
+
try {
|
|
1879
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
1880
|
+
method: 'POST',
|
|
1881
|
+
headers: {
|
|
1882
|
+
authorization: `Bearer ${token}`,
|
|
1883
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
1884
|
+
'anthropic-version': '2023-06-01',
|
|
1885
|
+
'content-type': 'application/json',
|
|
1886
|
+
},
|
|
1887
|
+
body: JSON.stringify({ model: CLAUDE_USAGE_PROBE_MODEL, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
|
|
1888
|
+
signal: controller.signal,
|
|
1889
|
+
});
|
|
1890
|
+
// The unified rate-limit headers ride on every response (incl. 429), so parse them before
|
|
1891
|
+
// worrying about the status code. Drain the body so the socket can be reused/closed.
|
|
1892
|
+
const usage = usageFromRatelimitHeaders(res.headers);
|
|
1893
|
+
void res.text().catch(() => { });
|
|
1894
|
+
return usage;
|
|
1895
|
+
}
|
|
1896
|
+
catch {
|
|
1897
|
+
return null;
|
|
1898
|
+
}
|
|
1899
|
+
finally {
|
|
1900
|
+
clearTimeout(timer);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
export function claudeUsageForToken(token) {
|
|
1904
|
+
const t = String(token || '');
|
|
1905
|
+
if (!t)
|
|
1906
|
+
return Promise.resolve(null);
|
|
1907
|
+
const now = Date.now();
|
|
1908
|
+
const cached = claudeTokenUsageCache.get(t);
|
|
1909
|
+
if (cached) {
|
|
1910
|
+
const ttl = cached.ok ? TOKEN_USAGE_OK_TTL_MS : TOKEN_USAGE_RETRY_TTL_MS;
|
|
1911
|
+
if (now - cached.at < ttl)
|
|
1912
|
+
return Promise.resolve(cached.value);
|
|
1913
|
+
}
|
|
1914
|
+
const inflight = claudeTokenUsageInflight.get(t);
|
|
1915
|
+
if (inflight)
|
|
1916
|
+
return inflight;
|
|
1917
|
+
const p = (async () => {
|
|
1918
|
+
const fresh = await claudeUsageFromInferenceHeaders(t);
|
|
1919
|
+
const at = Date.now();
|
|
1920
|
+
if (fresh) {
|
|
1921
|
+
claudeTokenUsageCache.set(t, { value: fresh, at, ok: true });
|
|
1922
|
+
return fresh;
|
|
1923
|
+
}
|
|
1924
|
+
// Probe failed (network / transient): keep serving last-good, but back off retries.
|
|
1925
|
+
const value = claudeTokenUsageCache.get(t)?.value ?? null;
|
|
1926
|
+
claudeTokenUsageCache.set(t, { value, at, ok: false });
|
|
1927
|
+
return value;
|
|
1928
|
+
})().finally(() => claudeTokenUsageInflight.delete(t));
|
|
1929
|
+
claudeTokenUsageInflight.set(t, p);
|
|
1930
|
+
return p;
|
|
1931
|
+
}
|
|
1827
1932
|
function getClaudeUsageFromTelemetry(home, model) {
|
|
1828
1933
|
const telemetryRoot = path.join(home, '.claude', 'telemetry');
|
|
1829
1934
|
if (!fs.existsSync(telemetryRoot))
|
|
@@ -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
|
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))
|