opencandle 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/analysts/orchestrator.d.ts +2 -0
- package/dist/analysts/orchestrator.js +19 -0
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/doctor/report.js +1 -1
- package/dist/doctor/report.js.map +1 -1
- package/dist/infra/cache.d.ts +6 -0
- package/dist/infra/cache.js +16 -6
- package/dist/infra/cache.js.map +1 -1
- package/dist/infra/freshness.d.ts +21 -0
- package/dist/infra/freshness.js +119 -0
- package/dist/infra/freshness.js.map +1 -0
- package/dist/infra/market-calendar.d.ts +14 -0
- package/dist/infra/market-calendar.js +137 -0
- package/dist/infra/market-calendar.js.map +1 -0
- package/dist/infra/rate-limiter.js +1 -0
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/market-state/summaries.d.ts +8 -0
- package/dist/market-state/summaries.js +70 -0
- package/dist/market-state/summaries.js.map +1 -0
- package/dist/onboarding/providers.d.ts +25 -1
- package/dist/onboarding/providers.js +26 -0
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/pi/opencandle-extension.js +44 -5
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/providers/alpha-vantage.js +6 -0
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/coingecko.js +1 -0
- package/dist/providers/coingecko.js.map +1 -1
- package/dist/providers/polymarket.d.ts +2 -0
- package/dist/providers/polymarket.js +163 -0
- package/dist/providers/polymarket.js.map +1 -0
- package/dist/providers/wrap-provider.js +5 -3
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +3 -0
- package/dist/providers/yahoo-finance.js +206 -10
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/entity-extractor.js +16 -0
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/route-manifest.js +1 -1
- package/dist/routing/route-manifest.js.map +1 -1
- package/dist/routing/router-llm-client.js +18 -3
- package/dist/routing/router-llm-client.js.map +1 -1
- package/dist/routing/router.d.ts +1 -1
- package/dist/routing/router.js +274 -5
- package/dist/routing/router.js.map +1 -1
- package/dist/routing/slot-resolver.d.ts +1 -0
- package/dist/routing/slot-resolver.js +1 -1
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/runtime/evidence.d.ts +1 -0
- package/dist/runtime/evidence.js.map +1 -1
- package/dist/runtime/numeric-claims.d.ts +23 -0
- package/dist/runtime/numeric-claims.js +100 -0
- package/dist/runtime/numeric-claims.js.map +1 -0
- package/dist/runtime/planning-evidence.js +1 -76
- package/dist/runtime/planning-evidence.js.map +1 -1
- package/dist/runtime/prompt-step.d.ts +10 -1
- package/dist/runtime/prompt-step.js +116 -2
- package/dist/runtime/prompt-step.js.map +1 -1
- package/dist/runtime/session-coordinator.d.ts +11 -1
- package/dist/runtime/session-coordinator.js +352 -49
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/validation.d.ts +3 -1
- package/dist/runtime/workflow-events.d.ts +1 -1
- package/dist/runtime/workflow-events.js.map +1 -1
- package/dist/runtime/workflow-types.d.ts +3 -0
- package/dist/runtime/workflow-types.js.map +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -0
- package/dist/tools/fundamentals/comps.js +20 -9
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.js +183 -112
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/macro/event-probabilities.d.ts +8 -0
- package/dist/tools/macro/event-probabilities.js +121 -0
- package/dist/tools/macro/event-probabilities.js.map +1 -0
- package/dist/tools/market/crypto-price.d.ts +4 -1
- package/dist/tools/market/crypto-price.js +10 -1
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/screen-stocks.js +14 -3
- package/dist/tools/market/screen-stocks.js.map +1 -1
- package/dist/tools/market/stock-quote.d.ts +4 -1
- package/dist/tools/market/stock-quote.js +9 -4
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/option-chain.d.ts +4 -1
- package/dist/tools/options/option-chain.js +12 -1
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/risk-analysis.js +1 -1
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/technical/indicators.js +1 -1
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/fundamentals.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/market.d.ts +2 -0
- package/dist/types/options.d.ts +1 -0
- package/dist/types/prediction-markets.d.ts +13 -0
- package/dist/types/prediction-markets.js +2 -0
- package/dist/types/prediction-markets.js.map +1 -0
- package/gui/server/chat-event-adapter.ts +89 -5
- package/gui/server/http-routes.ts +285 -37
- package/gui/server/invoke-tool.ts +14 -13
- package/gui/server/live-chat-event-adapter.ts +37 -2
- package/gui/server/market-state-api.ts +63 -5
- package/gui/server/projector.ts +132 -3
- package/gui/server/session-actions.ts +32 -40
- package/gui/server/session-entry-wait.ts +22 -3
- package/gui/server/ws-hub.ts +16 -4
- package/gui/shared/chat-events.ts +10 -1
- package/gui/shared/event-reducer.ts +1 -0
- package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
- package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
- package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
- package/gui/web/dist/index.html +2 -2
- package/package.json +8 -13
- package/src/analysts/orchestrator.ts +20 -0
- package/src/doctor/report.ts +1 -1
- package/src/infra/cache.ts +24 -11
- package/src/infra/freshness.ts +165 -0
- package/src/infra/market-calendar.ts +193 -0
- package/src/infra/rate-limiter.ts +1 -0
- package/src/market-state/summaries.ts +75 -0
- package/src/onboarding/providers.ts +29 -1
- package/src/pi/opencandle-extension.ts +55 -5
- package/src/providers/alpha-vantage.ts +6 -0
- package/src/providers/coingecko.ts +2 -0
- package/src/providers/polymarket.ts +214 -0
- package/src/providers/wrap-provider.ts +5 -3
- package/src/providers/yahoo-finance.ts +339 -27
- package/src/routing/entity-extractor.ts +17 -0
- package/src/routing/route-manifest.ts +1 -1
- package/src/routing/router-llm-client.ts +31 -19
- package/src/routing/router.ts +340 -5
- package/src/routing/slot-resolver.ts +1 -1
- package/src/runtime/evidence.ts +1 -0
- package/src/runtime/numeric-claims.ts +108 -0
- package/src/runtime/planning-evidence.ts +6 -91
- package/src/runtime/prompt-step.ts +127 -2
- package/src/runtime/session-coordinator.ts +462 -57
- package/src/runtime/validation.ts +1 -1
- package/src/runtime/workflow-events.ts +2 -0
- package/src/runtime/workflow-types.ts +3 -0
- package/src/tools/fundamentals/comps.ts +25 -14
- package/src/tools/fundamentals/dcf.ts +204 -124
- package/src/tools/index.ts +3 -0
- package/src/tools/macro/event-probabilities.ts +141 -0
- package/src/tools/market/crypto-price.ts +14 -2
- package/src/tools/market/screen-stocks.ts +14 -3
- package/src/tools/market/stock-quote.ts +49 -43
- package/src/tools/options/option-chain.ts +16 -2
- package/src/tools/portfolio/risk-analysis.ts +1 -1
- package/src/tools/technical/indicators.ts +1 -1
- package/src/types/fundamentals.ts +1 -0
- package/src/types/index.ts +1 -0
- package/src/types/market.ts +2 -0
- package/src/types/options.ts +1 -0
- package/src/types/prediction-markets.ts +13 -0
- package/gui/server/chat-run-session.ts +0 -16
- package/gui/web/dist/assets/CatalogOverlay-ChRTKNlf.js +0 -1
- package/gui/web/dist/assets/index-BzyqyVnd.css +0 -2
- package/gui/web/dist/assets/index-DvMjkOP9.js +0 -65
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type Database from "better-sqlite3";
|
|
2
|
+
import { buildFreshnessStamp } from "../../src/infra/freshness.js";
|
|
2
3
|
import { isZeroFilledQuote, searchYahooInstruments } from "../../src/market-state/resolve.js";
|
|
3
4
|
import { MarketStateService } from "../../src/market-state/service.js";
|
|
4
5
|
import { initDefaultDatabase } from "../../src/memory/sqlite.js";
|
|
@@ -37,12 +38,12 @@ export interface MarketStateQuoteSnapshot {
|
|
|
37
38
|
instrumentId: number;
|
|
38
39
|
symbol: string;
|
|
39
40
|
status: "ok" | "unavailable";
|
|
40
|
-
currentPrice?: number;
|
|
41
|
+
currentPrice?: number | null;
|
|
41
42
|
changePercent?: number;
|
|
42
|
-
marketValue?: number;
|
|
43
|
+
marketValue?: number | null;
|
|
43
44
|
totalCost: number;
|
|
44
|
-
pnl?: number;
|
|
45
|
-
pnlPercent?: number;
|
|
45
|
+
pnl?: number | null;
|
|
46
|
+
pnlPercent?: number | null;
|
|
46
47
|
allocationPercent?: number;
|
|
47
48
|
currency: string;
|
|
48
49
|
includedInTotals: boolean;
|
|
@@ -60,6 +61,30 @@ export interface MarketStateQuoteSnapshot {
|
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
interface SavedSymbolsMemoOptions {
|
|
65
|
+
ttlMs?: number;
|
|
66
|
+
now?: () => number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createSavedSymbolsMemo(
|
|
70
|
+
load: () => string[],
|
|
71
|
+
options: SavedSymbolsMemoOptions = {},
|
|
72
|
+
): () => string[] {
|
|
73
|
+
const ttlMs = options.ttlMs ?? 30_000;
|
|
74
|
+
const now = options.now ?? Date.now;
|
|
75
|
+
let cachedAt = 0;
|
|
76
|
+
let cached: string[] | null = null;
|
|
77
|
+
return () => {
|
|
78
|
+
const current = now();
|
|
79
|
+
if (cached && current - cachedAt < ttlMs) return cached;
|
|
80
|
+
cached = load();
|
|
81
|
+
cachedAt = current;
|
|
82
|
+
return cached;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const getSavedMarketStateSymbols = createSavedSymbolsMemo(loadSavedMarketStateSymbols);
|
|
87
|
+
|
|
63
88
|
export function buildMarketStateSnapshot(db?: Database.Database): MarketStateSnapshot {
|
|
64
89
|
const ownedDb = db ?? initDefaultDatabase();
|
|
65
90
|
const service = new MarketStateService(ownedDb);
|
|
@@ -88,6 +113,30 @@ export function buildMarketStateSnapshot(db?: Database.Database): MarketStateSna
|
|
|
88
113
|
}
|
|
89
114
|
}
|
|
90
115
|
|
|
116
|
+
function loadSavedMarketStateSymbols(): string[] {
|
|
117
|
+
const db = initDefaultDatabase();
|
|
118
|
+
const service = new MarketStateService(db);
|
|
119
|
+
try {
|
|
120
|
+
const symbols = [
|
|
121
|
+
...service.listWatchlistItems().map((item) => item.symbol),
|
|
122
|
+
...service.listPortfolioLots().map((lot) => lot.symbol),
|
|
123
|
+
];
|
|
124
|
+
return normalizeSymbols(symbols);
|
|
125
|
+
} finally {
|
|
126
|
+
db.close();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeSymbols(symbols: string[]): string[] {
|
|
131
|
+
const normalized: string[] = [];
|
|
132
|
+
for (const symbol of symbols) {
|
|
133
|
+
const next = symbol.trim().toUpperCase();
|
|
134
|
+
if (!next || normalized.includes(next)) continue;
|
|
135
|
+
normalized.push(next);
|
|
136
|
+
}
|
|
137
|
+
return normalized;
|
|
138
|
+
}
|
|
139
|
+
|
|
91
140
|
export async function buildMarketStateQuoteSnapshot(
|
|
92
141
|
db?: Database.Database,
|
|
93
142
|
): Promise<MarketStateQuoteSnapshot> {
|
|
@@ -262,11 +311,20 @@ async function fetchQuoteSnapshot(symbol: string): Promise<
|
|
|
262
311
|
if (isZeroFilledQuote(result.data)) {
|
|
263
312
|
return { status: "unavailable", reason: "Yahoo returned no valid market data." };
|
|
264
313
|
}
|
|
314
|
+
const freshness = buildFreshnessStamp({
|
|
315
|
+
asOf: result.data.asOf,
|
|
316
|
+
cached: result.cached,
|
|
317
|
+
stale: result.stale,
|
|
318
|
+
cachedAt: result.timestamp,
|
|
319
|
+
});
|
|
320
|
+
if (freshness.isStaleForSession) {
|
|
321
|
+
return { status: "unavailable", reason: "provider returned stale market data" };
|
|
322
|
+
}
|
|
265
323
|
return {
|
|
266
324
|
status: "ok",
|
|
267
325
|
price: result.data.price,
|
|
268
326
|
changePercent: result.data.changePercent,
|
|
269
|
-
fetchedAt: result.timestamp,
|
|
327
|
+
fetchedAt: freshness.providerDataAt ?? result.timestamp,
|
|
270
328
|
stale: result.stale,
|
|
271
329
|
currency: result.data.currency ?? null,
|
|
272
330
|
};
|
package/gui/server/projector.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Message, ToolResultMessage } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
4
|
export interface DashboardState {
|
|
5
|
+
knownSymbols: string[];
|
|
5
6
|
watchlist: Array<{
|
|
6
7
|
symbol: string;
|
|
7
8
|
quote: Record<string, unknown> | null;
|
|
@@ -26,6 +27,16 @@ export interface DashboardState {
|
|
|
26
27
|
softGaps: Array<{ provider: string; lastSeen: string }>;
|
|
27
28
|
hardSkips: Array<{ provider: string; lastSeen: string }>;
|
|
28
29
|
};
|
|
30
|
+
lastTurn?: {
|
|
31
|
+
routeKind: string;
|
|
32
|
+
workflow?: string;
|
|
33
|
+
symbols: string[];
|
|
34
|
+
slotSources: Record<string, number>;
|
|
35
|
+
priorTurnCount: number;
|
|
36
|
+
savedStateIncluded?: boolean;
|
|
37
|
+
attachmentCount?: number;
|
|
38
|
+
validation?: { passed: boolean; mismatchCount: number };
|
|
39
|
+
};
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
const DIRECT_TOOL_GAP_PROVIDERS: Record<string, string> = {
|
|
@@ -41,6 +52,7 @@ const DIRECT_TOOL_GAP_PROVIDERS: Record<string, string> = {
|
|
|
41
52
|
|
|
42
53
|
export function createEmptyDashboardState(): DashboardState {
|
|
43
54
|
return {
|
|
55
|
+
knownSymbols: [],
|
|
44
56
|
watchlist: [],
|
|
45
57
|
activeAnalyses: [],
|
|
46
58
|
recentResearch: [],
|
|
@@ -48,12 +60,26 @@ export function createEmptyDashboardState(): DashboardState {
|
|
|
48
60
|
};
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
export function projectDashboard(
|
|
63
|
+
export function projectDashboard(
|
|
64
|
+
entries: SessionEntry[],
|
|
65
|
+
sessionId = "local",
|
|
66
|
+
savedSymbols: string[] = [],
|
|
67
|
+
): DashboardState {
|
|
52
68
|
const state = createEmptyDashboardState();
|
|
69
|
+
let pendingAttachmentCount: number | undefined;
|
|
70
|
+
let pendingAttachmentUserSeen = false;
|
|
53
71
|
|
|
54
72
|
for (const entry of entries) {
|
|
55
73
|
if (entry.type === "message") {
|
|
56
74
|
projectMessage(state, entry.message as Message, entry.timestamp, sessionId);
|
|
75
|
+
const message = entry.message as Message;
|
|
76
|
+
if (pendingAttachmentCount !== undefined) {
|
|
77
|
+
if (message.role === "user") pendingAttachmentUserSeen = true;
|
|
78
|
+
else if (pendingAttachmentUserSeen) {
|
|
79
|
+
pendingAttachmentCount = undefined;
|
|
80
|
+
pendingAttachmentUserSeen = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
57
83
|
continue;
|
|
58
84
|
}
|
|
59
85
|
|
|
@@ -73,6 +99,54 @@ export function projectDashboard(entries: SessionEntry[], sessionId = "local"):
|
|
|
73
99
|
continue;
|
|
74
100
|
}
|
|
75
101
|
|
|
102
|
+
if (entry.type === "custom" && entry.customType === "opencandle-user-input") {
|
|
103
|
+
const attachments = asArray(asRecord(entry.data).attachments);
|
|
104
|
+
pendingAttachmentCount = attachments.length > 0 ? attachments.length : undefined;
|
|
105
|
+
pendingAttachmentUserSeen = false;
|
|
106
|
+
if (state.lastTurn) {
|
|
107
|
+
if (pendingAttachmentCount !== undefined)
|
|
108
|
+
state.lastTurn.attachmentCount = pendingAttachmentCount;
|
|
109
|
+
else delete state.lastTurn.attachmentCount;
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (entry.type === "custom" && entry.customType === "opencandle-route-context") {
|
|
115
|
+
state.lastTurn = projectRouteContext(entry.data, pendingAttachmentCount);
|
|
116
|
+
addKnownSymbols(state, asArray(asRecord(asRecord(entry.data).entities).symbols));
|
|
117
|
+
pendingAttachmentCount = undefined;
|
|
118
|
+
pendingAttachmentUserSeen = false;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (pendingAttachmentUserSeen) {
|
|
123
|
+
pendingAttachmentCount = undefined;
|
|
124
|
+
pendingAttachmentUserSeen = false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (entry.type === "custom" && entry.customType === "opencandle-validation") {
|
|
128
|
+
if (state.lastTurn) {
|
|
129
|
+
const data = asRecord(entry.data);
|
|
130
|
+
state.lastTurn.validation = {
|
|
131
|
+
passed: Boolean(data.passed),
|
|
132
|
+
mismatchCount: asArray(data.mismatches).length,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (entry.type === "custom" && entry.customType === "opencandle-analyst-step") {
|
|
139
|
+
// debate_* stages share this entry type but are not analysts.
|
|
140
|
+
const stage = stringValue(asRecord(entry.data).stage) ?? "";
|
|
141
|
+
const active = state.activeAnalyses[state.activeAnalyses.length - 1];
|
|
142
|
+
if (active && stage.startsWith("analyst_")) {
|
|
143
|
+
const nextDone = active.analystsDone + 1;
|
|
144
|
+
active.analystsDone =
|
|
145
|
+
active.analystsTotal > 0 ? Math.min(nextDone, active.analystsTotal) : nextDone;
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
76
150
|
if (entry.type === "custom" && entry.customType === "opencandle-turn-gap") {
|
|
77
151
|
// The accumulator writes a single combined annotation string with one
|
|
78
152
|
// [OPENCANDLE_SKIPPED ... provider=X ...] tag per fallback provider.
|
|
@@ -105,9 +179,40 @@ export function projectDashboard(entries: SessionEntry[], sessionId = "local"):
|
|
|
105
179
|
}
|
|
106
180
|
}
|
|
107
181
|
|
|
182
|
+
addKnownSymbols(state, savedSymbols);
|
|
108
183
|
return state;
|
|
109
184
|
}
|
|
110
185
|
|
|
186
|
+
function projectRouteContext(
|
|
187
|
+
data: unknown,
|
|
188
|
+
attachmentCount: number | undefined,
|
|
189
|
+
): DashboardState["lastTurn"] {
|
|
190
|
+
const record = asRecord(data);
|
|
191
|
+
const entities = asRecord(record.entities);
|
|
192
|
+
const routeKind = stringValue(record.routeKind) ?? "unknown";
|
|
193
|
+
const workflow = stringValue(record.workflow);
|
|
194
|
+
const slots = asRecord(record.slots);
|
|
195
|
+
const slotSources: Record<string, number> = {};
|
|
196
|
+
for (const slot of Object.values(slots)) {
|
|
197
|
+
const source = stringValue(asRecord(slot).source);
|
|
198
|
+
if (!source) continue;
|
|
199
|
+
slotSources[source] = (slotSources[source] ?? 0) + 1;
|
|
200
|
+
}
|
|
201
|
+
const symbols = stringArray(entities.symbols);
|
|
202
|
+
const priorTurnCount = asArray(record.priorTurns).length;
|
|
203
|
+
return {
|
|
204
|
+
routeKind,
|
|
205
|
+
...(workflow ? { workflow } : {}),
|
|
206
|
+
symbols,
|
|
207
|
+
slotSources,
|
|
208
|
+
priorTurnCount,
|
|
209
|
+
...(typeof record.savedStateIncluded === "boolean"
|
|
210
|
+
? { savedStateIncluded: record.savedStateIncluded }
|
|
211
|
+
: {}),
|
|
212
|
+
...(attachmentCount !== undefined ? { attachmentCount } : {}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
111
216
|
function projectMessage(
|
|
112
217
|
state: DashboardState,
|
|
113
218
|
message: Message,
|
|
@@ -185,10 +290,13 @@ function projectQuote(
|
|
|
185
290
|
): void {
|
|
186
291
|
const symbol = symbolHint ?? inferSymbolFromContent(content);
|
|
187
292
|
if (!symbol) return;
|
|
293
|
+
const normalizedSymbol = normalizeSymbol(symbol);
|
|
294
|
+
if (!normalizedSymbol) return;
|
|
295
|
+
addKnownSymbols(state, [normalizedSymbol]);
|
|
188
296
|
|
|
189
|
-
const existing = state.watchlist.find((row) => row.symbol ===
|
|
297
|
+
const existing = state.watchlist.find((row) => row.symbol === normalizedSymbol);
|
|
190
298
|
const row = {
|
|
191
|
-
symbol,
|
|
299
|
+
symbol: normalizedSymbol,
|
|
192
300
|
quote: Object.keys(details).length > 0 ? details : null,
|
|
193
301
|
pinned: existing?.pinned ?? false,
|
|
194
302
|
lastSeen: timestamp,
|
|
@@ -240,6 +348,21 @@ function inferSymbolFromContent(content: ToolResultMessage["content"]): string |
|
|
|
240
348
|
return match?.[1];
|
|
241
349
|
}
|
|
242
350
|
|
|
351
|
+
function addKnownSymbols(state: DashboardState, values: unknown[]): void {
|
|
352
|
+
for (const value of values) {
|
|
353
|
+
const symbol = normalizeSymbol(value);
|
|
354
|
+
if (!symbol || state.knownSymbols.includes(symbol)) continue;
|
|
355
|
+
if (state.knownSymbols.length >= 100) return;
|
|
356
|
+
state.knownSymbols.push(symbol);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function normalizeSymbol(value: unknown): string | null {
|
|
361
|
+
if (typeof value !== "string") return null;
|
|
362
|
+
const normalized = value.trim().toUpperCase();
|
|
363
|
+
return normalized ? normalized : null;
|
|
364
|
+
}
|
|
365
|
+
|
|
243
366
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
244
367
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
245
368
|
? (value as Record<string, unknown>)
|
|
@@ -258,6 +381,12 @@ function firstString(value: unknown): string | undefined {
|
|
|
258
381
|
return Array.isArray(value) ? value.find((item) => typeof item === "string") : undefined;
|
|
259
382
|
}
|
|
260
383
|
|
|
384
|
+
function stringArray(value: unknown): string[] {
|
|
385
|
+
return Array.isArray(value)
|
|
386
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
387
|
+
: [];
|
|
388
|
+
}
|
|
389
|
+
|
|
261
390
|
function numberValue(value: unknown): number | undefined {
|
|
262
391
|
return typeof value === "number" ? value : undefined;
|
|
263
392
|
}
|
|
@@ -29,7 +29,6 @@ interface SessionActionClient {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface SessionActionsController {
|
|
32
|
-
handlePrompt(prompt: string): Promise<void>;
|
|
33
32
|
handleAskUserAnswer(id: string, value: unknown, action?: SessionActionMeta): Promise<void>;
|
|
34
33
|
handleAskUserCancel(id: string, action?: SessionActionMeta): Promise<void>;
|
|
35
34
|
handleNewSession(): Promise<void>;
|
|
@@ -80,32 +79,6 @@ export function createSessionActionsController({
|
|
|
80
79
|
if (role !== "writer") throw new Error("Read-only follower mode");
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
async function handlePrompt(prompt: string): Promise<void> {
|
|
84
|
-
ensureWriter();
|
|
85
|
-
|
|
86
|
-
const modelSetup = getModelSetupState();
|
|
87
|
-
const sessionManager = getSessionManager();
|
|
88
|
-
const trimmedPrompt = prompt.trim();
|
|
89
|
-
if (!trimmedPrompt.startsWith("/") && modelSetup.requirement !== "ready") {
|
|
90
|
-
sessionManager.appendMessage({ role: "user", content: prompt, timestamp: now() });
|
|
91
|
-
broadcastState();
|
|
92
|
-
const message =
|
|
93
|
-
modelSetup.requirement === "select_model"
|
|
94
|
-
? "Choose an available model before chat can run. OpenCandle found configured credentials but no active model."
|
|
95
|
-
: "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
|
|
96
|
-
sessionManager.appendCustomMessageEntry("opencandle-model-setup", message, true, {
|
|
97
|
-
source: "gui",
|
|
98
|
-
requirement: modelSetup.requirement,
|
|
99
|
-
});
|
|
100
|
-
broadcastState();
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const beforeIds = new Set(sessionManager.getEntries().map((entry) => entry.id));
|
|
105
|
-
await promptAndSettle(getSession(), prompt, beforeIds);
|
|
106
|
-
broadcastState();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
82
|
async function handleAskUserAnswer(
|
|
110
83
|
id: string,
|
|
111
84
|
value: unknown,
|
|
@@ -172,7 +145,6 @@ export function createSessionActionsController({
|
|
|
172
145
|
}
|
|
173
146
|
|
|
174
147
|
return {
|
|
175
|
-
handlePrompt,
|
|
176
148
|
handleAskUserAnswer,
|
|
177
149
|
handleAskUserCancel,
|
|
178
150
|
handleNewSession,
|
|
@@ -191,7 +163,8 @@ export function createSessionActionsController({
|
|
|
191
163
|
await handler();
|
|
192
164
|
return;
|
|
193
165
|
}
|
|
194
|
-
const sessionId = action.sessionId?.trim()
|
|
166
|
+
const sessionId = action.sessionId?.trim();
|
|
167
|
+
if (!sessionId) throw new Error("sessionId is required");
|
|
195
168
|
const result = await localSessionCoordinator.runSessionAction(
|
|
196
169
|
{
|
|
197
170
|
sessionId,
|
|
@@ -230,7 +203,7 @@ export function createSessionActionsController({
|
|
|
230
203
|
"x-opencandle-coordinator-secret": lock.coordinatorSecret,
|
|
231
204
|
},
|
|
232
205
|
body: JSON.stringify({
|
|
233
|
-
sessionId: action?.sessionId
|
|
206
|
+
sessionId: action?.sessionId,
|
|
234
207
|
actionId: action?.actionId || "",
|
|
235
208
|
actionType,
|
|
236
209
|
payload,
|
|
@@ -258,7 +231,8 @@ export function createSessionActionsController({
|
|
|
258
231
|
if (!targetSessionId || targetSessionId === current.getSessionId()) return current;
|
|
259
232
|
const sessions = await SessionManager.list(cwd, sessionDir);
|
|
260
233
|
const match = sessions.find((candidate) => candidate.id === targetSessionId);
|
|
261
|
-
|
|
234
|
+
if (!match) throw new Error("Unknown saved session");
|
|
235
|
+
return SessionManager.open(match.path, sessionDir, cwd);
|
|
262
236
|
}
|
|
263
237
|
}
|
|
264
238
|
|
|
@@ -267,12 +241,10 @@ export async function promptAndSettle(
|
|
|
267
241
|
prompt: string,
|
|
268
242
|
beforeIds: Set<string>,
|
|
269
243
|
observation?: PromptObservation,
|
|
244
|
+
options?: { images?: Array<{ type: "image"; data: string; mimeType: string }> },
|
|
270
245
|
): Promise<void> {
|
|
271
|
-
await runSession.prompt(prompt);
|
|
272
|
-
await
|
|
273
|
-
isStreaming: runSession.isStreaming,
|
|
274
|
-
pendingMessageCount: runSession.pendingMessageCount,
|
|
275
|
-
}));
|
|
246
|
+
await runSession.prompt(prompt, options);
|
|
247
|
+
await settleWithEventProgress(runSession);
|
|
276
248
|
await waitForNewEntryId(
|
|
277
249
|
() => runSession.sessionManager.getEntries().map((entry) => entry.id),
|
|
278
250
|
beforeIds,
|
|
@@ -281,6 +253,27 @@ export async function promptAndSettle(
|
|
|
281
253
|
await replayObservedWorkflowPromptIfNeeded(runSession, prompt, observation);
|
|
282
254
|
}
|
|
283
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Settle wait fed by a session-event counter: a single long model generation
|
|
258
|
+
* keeps isStreaming/pendingMessageCount frozen for its whole duration, and
|
|
259
|
+
* without an activity signal the stall detector killed healthy long turns.
|
|
260
|
+
*/
|
|
261
|
+
async function settleWithEventProgress(runSession: AgentSession): Promise<void> {
|
|
262
|
+
let progressToken = 0;
|
|
263
|
+
const unsubscribe = runSession.subscribe(() => {
|
|
264
|
+
progressToken += 1;
|
|
265
|
+
});
|
|
266
|
+
try {
|
|
267
|
+
await waitForSessionTurnSettlement(() => ({
|
|
268
|
+
isStreaming: runSession.isStreaming,
|
|
269
|
+
pendingMessageCount: runSession.pendingMessageCount,
|
|
270
|
+
progressToken,
|
|
271
|
+
}));
|
|
272
|
+
} finally {
|
|
273
|
+
unsubscribe();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
284
277
|
export async function replayObservedWorkflowPromptIfNeeded(
|
|
285
278
|
runSession: AgentSession,
|
|
286
279
|
originalPrompt: string,
|
|
@@ -290,14 +283,13 @@ export async function replayObservedWorkflowPromptIfNeeded(
|
|
|
290
283
|
const replayPrompt = selectReplayPrompt(observation, originalPrompt);
|
|
291
284
|
if (!replayPrompt) return;
|
|
292
285
|
|
|
286
|
+
// V1 attachment replay is text-only: the recorded expanded prompt is
|
|
287
|
+
// re-sent, but image bytes are not replayed.
|
|
293
288
|
await runSession.prompt(replayPrompt, {
|
|
294
289
|
expandPromptTemplates: false,
|
|
295
290
|
source: "extension",
|
|
296
291
|
});
|
|
297
|
-
await
|
|
298
|
-
isStreaming: runSession.isStreaming,
|
|
299
|
-
pendingMessageCount: runSession.pendingMessageCount,
|
|
300
|
-
}));
|
|
292
|
+
await settleWithEventProgress(runSession);
|
|
301
293
|
await waitForResolvedToolCalls(() => runSession.sessionManager.getEntries());
|
|
302
294
|
}
|
|
303
295
|
|
|
@@ -12,6 +12,12 @@ export interface WaitForSessionTurnSettlementOptions extends WaitForEntryCountOp
|
|
|
12
12
|
export interface SessionRunStatus {
|
|
13
13
|
isStreaming: boolean;
|
|
14
14
|
pendingMessageCount: number;
|
|
15
|
+
/**
|
|
16
|
+
* Monotonic activity signal (e.g. a session-event counter). A single long
|
|
17
|
+
* model generation keeps isStreaming/pendingMessageCount frozen for its
|
|
18
|
+
* whole duration; this token is what distinguishes it from a hung run.
|
|
19
|
+
*/
|
|
20
|
+
progressToken?: number;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
export interface UnresolvedToolCall {
|
|
@@ -60,15 +66,29 @@ export async function waitForSessionTurnSettlement(
|
|
|
60
66
|
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
61
67
|
const intervalMs = options.intervalMs ?? 25;
|
|
62
68
|
const idleGraceMs = options.idleGraceMs ?? 250;
|
|
63
|
-
|
|
69
|
+
// timeoutMs bounds STALL, not total runtime: a live workflow run (e.g.
|
|
70
|
+
// /analyze) stays active for minutes while its status keeps changing, and
|
|
71
|
+
// capping total runtime failed those runs mid-workflow with
|
|
72
|
+
// "Timed out waiting for the session turn to settle". A hung session stops
|
|
73
|
+
// changing status entirely, which is what the deadline must catch.
|
|
74
|
+
let lastProgressAt = Date.now();
|
|
75
|
+
let lastSignature: string | undefined;
|
|
64
76
|
let idleSince: number | undefined;
|
|
65
77
|
|
|
66
|
-
while (
|
|
78
|
+
while (true) {
|
|
67
79
|
const status = getStatus();
|
|
80
|
+
const signature = `${status.isStreaming}:${status.pendingMessageCount}:${status.progressToken ?? 0}`;
|
|
81
|
+
if (signature !== lastSignature) {
|
|
82
|
+
lastSignature = signature;
|
|
83
|
+
lastProgressAt = Date.now();
|
|
84
|
+
}
|
|
68
85
|
const active = status.isStreaming || status.pendingMessageCount > 0;
|
|
69
86
|
|
|
70
87
|
if (active) {
|
|
71
88
|
idleSince = undefined;
|
|
89
|
+
if (Date.now() - lastProgressAt >= timeoutMs) {
|
|
90
|
+
throw new Error("Timed out waiting for the session turn to settle");
|
|
91
|
+
}
|
|
72
92
|
await delay(intervalMs);
|
|
73
93
|
continue;
|
|
74
94
|
}
|
|
@@ -80,7 +100,6 @@ export async function waitForSessionTurnSettlement(
|
|
|
80
100
|
|
|
81
101
|
await delay(intervalMs);
|
|
82
102
|
}
|
|
83
|
-
throw new Error("Timed out waiting for the session turn to settle");
|
|
84
103
|
}
|
|
85
104
|
|
|
86
105
|
export function findUnresolvedToolCalls(entries: SessionEntry[]): UnresolvedToolCall[] {
|
package/gui/server/ws-hub.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { ChatEvent } from "../shared/chat-events.js";
|
|
|
16
16
|
import type { BackgroundQuoteRefreshes } from "./background-quotes.js";
|
|
17
17
|
import { sessionEntriesToChatEvents } from "./chat-event-adapter.js";
|
|
18
18
|
import type { ToolInvokeController } from "./invoke-tool.js";
|
|
19
|
+
import { getSavedMarketStateSymbols } from "./market-state-api.js";
|
|
19
20
|
import type { ModelSetupController } from "./model-setup.js";
|
|
20
21
|
import { projectDashboard } from "./projector.js";
|
|
21
22
|
import type { SessionActionsController } from "./session-actions.js";
|
|
@@ -109,8 +110,11 @@ export function createWsHub({
|
|
|
109
110
|
try {
|
|
110
111
|
switch (data.type) {
|
|
111
112
|
case "chat.prompt":
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
// Legacy implicit-active-session mutation; parity with the HTTP
|
|
114
|
+
// route's 410. Chat runs go through the session-addressed API.
|
|
115
|
+
throw new Error(
|
|
116
|
+
"Legacy active-session chat prompts are no longer supported. Use the session-addressed chat-run API.",
|
|
117
|
+
);
|
|
114
118
|
case "ask_user.answer":
|
|
115
119
|
await sessionActionsController.handleAskUserAnswer(String(data.id ?? ""), data.answer, {
|
|
116
120
|
actionId: String(data.actionId ?? ""),
|
|
@@ -281,7 +285,11 @@ export function createWsHub({
|
|
|
281
285
|
const entries = sessionManager.getEntries();
|
|
282
286
|
return {
|
|
283
287
|
sessionId,
|
|
284
|
-
state: projectDashboard(
|
|
288
|
+
state: projectDashboard(
|
|
289
|
+
backgroundQuoteRefreshes.withEntries(entries),
|
|
290
|
+
sessionId,
|
|
291
|
+
getSavedMarketStateSymbols(),
|
|
292
|
+
),
|
|
285
293
|
entries,
|
|
286
294
|
events: currentChatEvents(entries),
|
|
287
295
|
// Ownership can change mid-run (e.g. a TUI takes over the session), so
|
|
@@ -295,7 +303,11 @@ export function createWsHub({
|
|
|
295
303
|
const entries = sessionManager.getEntries();
|
|
296
304
|
return {
|
|
297
305
|
sessionId,
|
|
298
|
-
state: projectDashboard(
|
|
306
|
+
state: projectDashboard(
|
|
307
|
+
backgroundQuoteRefreshes.withEntries(entries),
|
|
308
|
+
sessionId,
|
|
309
|
+
getSavedMarketStateSymbols(),
|
|
310
|
+
),
|
|
299
311
|
entries,
|
|
300
312
|
events: sessionEntriesToChatEvents(entries, {
|
|
301
313
|
sessionId,
|
|
@@ -12,12 +12,19 @@ export interface MessageToolContent {
|
|
|
12
12
|
|
|
13
13
|
export interface MessageImageContent {
|
|
14
14
|
type: "image";
|
|
15
|
-
url
|
|
15
|
+
url?: string;
|
|
16
|
+
data?: string;
|
|
17
|
+
mimeType?: string;
|
|
16
18
|
alt?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export type MessageContent = MessageTextContent | MessageToolContent | MessageImageContent;
|
|
20
22
|
|
|
23
|
+
export interface MessageAttachmentChip {
|
|
24
|
+
kind: string;
|
|
25
|
+
label: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
export interface ToolOutput {
|
|
22
29
|
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
|
23
30
|
details?: unknown;
|
|
@@ -63,6 +70,7 @@ export type ChatEvent =
|
|
|
63
70
|
type: "message.completed";
|
|
64
71
|
messageId: string;
|
|
65
72
|
content: MessageContent[];
|
|
73
|
+
attachments?: MessageAttachmentChip[];
|
|
66
74
|
runId?: string;
|
|
67
75
|
} & ChatEventBase)
|
|
68
76
|
| ({
|
|
@@ -98,6 +106,7 @@ export interface RenderMessage {
|
|
|
98
106
|
status: "streaming" | "completed";
|
|
99
107
|
content: MessageContent[];
|
|
100
108
|
text: string;
|
|
109
|
+
attachments?: MessageAttachmentChip[];
|
|
101
110
|
customType?: string;
|
|
102
111
|
}
|
|
103
112
|
|