@v587d/capital-generation 2.9.2 → 2.10.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +8 -5
  3. package/chart-ui/package.json +1 -1
  4. package/config/client.js +8 -6
  5. package/config/client.src.cjs +74 -34
  6. package/config/index.js +8 -2
  7. package/config/package.json +1 -1
  8. package/lib/agents/selected-skills-provider.d.ts +29 -10
  9. package/lib/agents/selected-skills-provider.js +262 -139
  10. package/lib/agents/settled-wake.d.ts +43 -0
  11. package/lib/agents/settled-wake.js +81 -0
  12. package/lib/agents/skill-usage.d.ts +22 -0
  13. package/lib/agents/skill-usage.js +92 -0
  14. package/lib/agents/wait-for-agents.d.ts +79 -0
  15. package/lib/agents/wait-for-agents.js +256 -0
  16. package/lib/capital-settings.d.ts +12 -1
  17. package/lib/capital-settings.js +23 -3
  18. package/lib/chart/artifact-ref.d.ts +4 -1
  19. package/lib/chart/artifact-ref.js +2 -1
  20. package/lib/chart/delivery.d.ts +15 -1
  21. package/lib/chart/delivery.js +17 -8
  22. package/lib/chart/events.d.ts +1 -1
  23. package/lib/chart/html.js +16 -0
  24. package/lib/chart/present-tool.js +52 -3
  25. package/lib/chart/tool.js +63 -9
  26. package/lib/data-collector/store.d.ts +1 -0
  27. package/lib/data-collector/store.js +2 -2
  28. package/lib/index.d.ts +6 -3
  29. package/lib/index.js +10 -19
  30. package/lib/orchestration-entry.d.ts +11 -0
  31. package/lib/orchestration-entry.js +6 -0
  32. package/lib/present-entry.js +26 -5
  33. package/lib/root-extras-entry.d.ts +2 -5
  34. package/lib/root-extras-entry.js +1 -3
  35. package/lib/selected-skills-entry.d.ts +14 -0
  36. package/lib/selected-skills-entry.js +43 -0
  37. package/lib/web-retriever/local-fetch.d.ts +1 -1
  38. package/lib/web-retriever/local-fetch.js +1 -1
  39. package/lib/web-retriever/wind-client.d.ts +1 -1
  40. package/lib/web-retriever/wind-client.js +1 -1
  41. package/package.json +15 -2
  42. package/preset/capital-generation/README.md +3 -3
  43. package/preset/capital-generation/agent.patch.yml +43 -20
  44. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +3 -2
  45. package/preset/capital-generation/skills/capital-data-query/SKILL.md +32 -0
  46. package/preset/capital-generation/skills/capital-equity-research/SKILL.md +5 -0
  47. package/preset/capital-generation/skills/capital-event-impact/SKILL.md +3 -0
  48. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +8 -10
  49. package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +3 -0
  50. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +7 -1
  51. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -0
  52. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +3 -0
  53. package/selected-skills/README.md +5 -4
  54. package/selected-skills/ROUTING.md +88 -0
  55. package/selected-skills/catalog.js +32 -121
  56. package/selected-skills/routing.js +1036 -0
  57. package/watchlist/package.json +1 -1
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Programmatic selected-skill ledger: a load counts only after the host returned the
3
+ * selected provider's body. The root prompt reads the tree; the report script re-derives
4
+ * the same facts from durable session logs, so model self-report is never the source.
5
+ */
6
+ import { type AgentLike } from './root-tool-policy.js';
7
+ export interface SkillUsageRow {
8
+ readonly sessionId: string;
9
+ readonly parentSession?: string;
10
+ readonly role: string;
11
+ readonly names: readonly string[];
12
+ }
13
+ /** Register a role Agent before it loads anything, so the tree edge survives an empty session. */
14
+ export declare function trackSelectedSkillUsage(agent: AgentLike | undefined, role: string): void;
15
+ export declare function recordSelectedSkillUsage(agent: AgentLike | undefined, role: string, name: string): void;
16
+ /** A new user turn drops the previous turn's names but keeps the tree edges. */
17
+ export declare function resetSelectedSkillUsage(rootSessionId: string): void;
18
+ /** Agent teardown: the whole subtree's records are process-local and must not outlive it. */
19
+ export declare function clearSelectedSkillUsage(rootSessionId: string): void;
20
+ export declare function clearAllSelectedSkillUsage(): void;
21
+ export declare function skillUsageRows(rootSessionId: string): SkillUsageRow[];
22
+ export declare function skillUsageNamesForSession(sessionId: string): string[];
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Programmatic selected-skill ledger: a load counts only after the host returned the
3
+ * selected provider's body. The root prompt reads the tree; the report script re-derives
4
+ * the same facts from durable session logs, so model self-report is never the source.
5
+ */
6
+ import { agentSessionId } from './root-tool-policy.js';
7
+ const records = new Map();
8
+ const bareSession = (id) => id.replace(/^session-/, '');
9
+ function recordFor(agent, role) {
10
+ const id = agentSessionId(agent);
11
+ if (id === undefined)
12
+ return undefined;
13
+ const key = bareSession(id);
14
+ const parent = agent?.session?.header?.parentSession;
15
+ const parentId = typeof parent === 'string' && parent.length > 0 ? bareSession(parent) : undefined;
16
+ const existing = records.get(key);
17
+ if (existing !== undefined) {
18
+ existing.role = role;
19
+ if (parentId !== undefined)
20
+ existing.parentSession = parentId;
21
+ return existing;
22
+ }
23
+ const created = {
24
+ sessionId: key,
25
+ ...(parentId === undefined ? {} : { parentSession: parentId }),
26
+ role,
27
+ names: [],
28
+ };
29
+ records.set(key, created);
30
+ return created;
31
+ }
32
+ /** Register a role Agent before it loads anything, so the tree edge survives an empty session. */
33
+ export function trackSelectedSkillUsage(agent, role) {
34
+ recordFor(agent, role);
35
+ }
36
+ export function recordSelectedSkillUsage(agent, role, name) {
37
+ if (name.length === 0)
38
+ return;
39
+ const record = recordFor(agent, role);
40
+ if (record === undefined)
41
+ return;
42
+ if (!record.names.includes(name))
43
+ record.names.push(name);
44
+ }
45
+ function rootOf(sessionId) {
46
+ let cursor = records.get(sessionId);
47
+ const seen = new Set();
48
+ while (cursor?.parentSession !== undefined && !seen.has(cursor.sessionId)) {
49
+ seen.add(cursor.sessionId);
50
+ const parent = records.get(cursor.parentSession);
51
+ if (parent === undefined)
52
+ break;
53
+ cursor = parent;
54
+ }
55
+ return cursor?.sessionId ?? sessionId;
56
+ }
57
+ /** A new user turn drops the previous turn's names but keeps the tree edges. */
58
+ export function resetSelectedSkillUsage(rootSessionId) {
59
+ const root = bareSession(rootSessionId);
60
+ for (const record of records.values()) {
61
+ if (rootOf(record.sessionId) === root)
62
+ record.names.length = 0;
63
+ }
64
+ }
65
+ /** Agent teardown: the whole subtree's records are process-local and must not outlive it. */
66
+ export function clearSelectedSkillUsage(rootSessionId) {
67
+ const root = bareSession(rootSessionId);
68
+ // Resolve every member BEFORE deleting: removing the root first would orphan its children
69
+ // and the remaining loop would no longer see them.
70
+ const doomed = [...records.values()]
71
+ .filter((record) => rootOf(record.sessionId) === root)
72
+ .map((record) => record.sessionId);
73
+ for (const id of doomed)
74
+ records.delete(id);
75
+ }
76
+ export function clearAllSelectedSkillUsage() {
77
+ records.clear();
78
+ }
79
+ export function skillUsageRows(rootSessionId) {
80
+ const root = bareSession(rootSessionId);
81
+ return [...records.values()]
82
+ .filter((record) => rootOf(record.sessionId) === root && record.names.length > 0)
83
+ .map((record) => ({
84
+ sessionId: record.sessionId,
85
+ ...(record.parentSession === undefined ? {} : { parentSession: record.parentSession }),
86
+ role: record.role,
87
+ names: [...record.names],
88
+ }));
89
+ }
90
+ export function skillUsageNamesForSession(sessionId) {
91
+ return [...(records.get(bareSession(sessionId))?.names ?? [])];
92
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Root-only `wait_for_agents`: hold one turn across child relays; returns on the first relay, on
3
+ * the tree settling, or after 30s. `waitForChildren` has no abort parameter; the slice races it.
4
+ * Host contract: docs/reference/dsh-surface-ledger.md L36.
5
+ */
6
+ import type { Context } from '@deepseek-ai/cordis';
7
+ export interface WaitInboxLike {
8
+ readonly nextTurn?: readonly unknown[];
9
+ readonly nextStep?: readonly unknown[];
10
+ }
11
+ export interface WaitAgentLike {
12
+ readonly session?: {
13
+ readonly id?: string;
14
+ readonly header?: {
15
+ readonly parentSession?: string;
16
+ readonly origin?: 'subagent';
17
+ };
18
+ };
19
+ readonly inbox?: WaitInboxLike;
20
+ readonly status?: 'idle' | 'running';
21
+ }
22
+ export interface WaitDescendantLike {
23
+ readonly kind?: string;
24
+ readonly id?: string;
25
+ readonly label?: string;
26
+ readonly activity?: string;
27
+ }
28
+ export interface WaitSubagentsLike {
29
+ waitForChildren(parent: WaitAgentLike): Promise<boolean>;
30
+ listDescendants?(rootSessionId: string, signal?: AbortSignal): Promise<readonly WaitDescendantLike[]>;
31
+ }
32
+ interface AgentsLike {
33
+ get?(id: string): {
34
+ readonly status?: 'idle' | 'running';
35
+ } | undefined;
36
+ }
37
+ interface EventContextLike {
38
+ on?(event: string, listener: (...args: unknown[]) => void): unknown;
39
+ }
40
+ export interface WaitForAgentsOptions {
41
+ /** Bounded slice; the model cannot extend it. */
42
+ readonly waitSliceMs?: number;
43
+ readonly now?: () => number;
44
+ }
45
+ export interface WaitForAgentsDeps extends WaitForAgentsOptions {
46
+ subagents: () => WaitSubagentsLike | undefined;
47
+ events?: EventContextLike;
48
+ agents?: () => AgentsLike | undefined;
49
+ }
50
+ export type WaitForAgentsStatus = 'all_settled' | 'message' | 'user_input' | 'timeout';
51
+ export interface WaitForAgentsReceipt {
52
+ readonly status: WaitForAgentsStatus;
53
+ readonly waited_ms: number;
54
+ readonly pending_messages: number;
55
+ readonly running: Array<{
56
+ readonly id: string;
57
+ readonly label?: string;
58
+ }>;
59
+ readonly hint: string;
60
+ }
61
+ declare function render(_args: unknown, value: unknown): Array<{
62
+ type: 'text';
63
+ text: string;
64
+ }>;
65
+ export declare function waitForAgentsToolDefinition(deps: WaitForAgentsDeps): {
66
+ name: string;
67
+ description: string;
68
+ parameters: object;
69
+ output: {
70
+ schema: object;
71
+ render: typeof render;
72
+ };
73
+ execute(_args: Record<string, unknown>, exec: {
74
+ signal: AbortSignal;
75
+ agent?: WaitAgentLike;
76
+ }): Promise<WaitForAgentsReceipt>;
77
+ };
78
+ export declare function registerWaitForAgentsTool(ctx: Context, options?: WaitForAgentsOptions): void;
79
+ export {};
@@ -0,0 +1,256 @@
1
+ const DEFAULT_WAIT_SLICE_MS = 30_000;
2
+ const MAX_WAIT_SLICE_MS = 60_000;
3
+ const jsonObject = (properties = {}, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
4
+ function render(_args, value) {
5
+ return [{ type: 'text', text: JSON.stringify(value) }];
6
+ }
7
+ function sourceKindOf(message) {
8
+ if (message === null || typeof message !== 'object')
9
+ return undefined;
10
+ const source = message.source;
11
+ if (source === null || typeof source !== 'object')
12
+ return undefined;
13
+ const kind = source.kind;
14
+ return typeof kind === 'string' ? kind : undefined;
15
+ }
16
+ /** Settlement notices are lifecycle noise here; only a human or a relayed result is actionable. */
17
+ function pendingCounts(agent) {
18
+ const messages = [...(agent.inbox?.nextTurn ?? []), ...(agent.inbox?.nextStep ?? [])];
19
+ let users = 0;
20
+ let children = 0;
21
+ for (const message of messages) {
22
+ const kind = sourceKindOf(message);
23
+ if (kind === 'user')
24
+ users += 1;
25
+ else if (kind === 'agent-message')
26
+ children += 1;
27
+ }
28
+ return { total: users + children, users, children };
29
+ }
30
+ function resolveSlice(value) {
31
+ const numeric = typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : DEFAULT_WAIT_SLICE_MS;
32
+ return Math.max(1, Math.min(MAX_WAIT_SLICE_MS, numeric));
33
+ }
34
+ function abortReason(signal) {
35
+ return signal.reason instanceof Error ? signal.reason : new Error('wait_for_agents aborted');
36
+ }
37
+ function waitTimer(ms, signal) {
38
+ let handle;
39
+ let onAbort;
40
+ const promise = new Promise((resolve, reject) => {
41
+ if (signal.aborted) {
42
+ reject(abortReason(signal));
43
+ return;
44
+ }
45
+ handle = setTimeout(() => resolve('timeout'), ms);
46
+ onAbort = () => {
47
+ if (handle !== undefined)
48
+ clearTimeout(handle);
49
+ reject(abortReason(signal));
50
+ };
51
+ signal.addEventListener('abort', onAbort, { once: true });
52
+ });
53
+ return {
54
+ promise,
55
+ dispose: () => {
56
+ if (handle !== undefined)
57
+ clearTimeout(handle);
58
+ if (onAbort !== undefined)
59
+ signal.removeEventListener('abort', onAbort);
60
+ },
61
+ };
62
+ }
63
+ async function runningRows(service, agentsOf, sessionId, signal) {
64
+ if (typeof service.listDescendants !== 'function')
65
+ return [];
66
+ let rows;
67
+ try {
68
+ rows = await service.listDescendants(sessionId, signal);
69
+ }
70
+ catch {
71
+ return [];
72
+ }
73
+ let agents;
74
+ try {
75
+ agents = agentsOf?.();
76
+ }
77
+ catch {
78
+ agents = undefined;
79
+ }
80
+ const seen = new Set();
81
+ const running = [];
82
+ for (const row of rows ?? []) {
83
+ if (row?.kind !== 'child' || typeof row.id !== 'string' || seen.has(row.id))
84
+ continue;
85
+ let live;
86
+ try {
87
+ live = agents?.get?.(row.id);
88
+ }
89
+ catch {
90
+ live = undefined;
91
+ }
92
+ // A live idle child is not progressing; a catalog row alone is only a historical child.
93
+ if (live?.status === 'idle')
94
+ continue;
95
+ if (row.activity !== 'running' && live?.status !== 'running')
96
+ continue;
97
+ seen.add(row.id);
98
+ const label = typeof row.label === 'string' && row.label.length > 0 ? row.label : undefined;
99
+ running.push({ id: row.id, ...(label === undefined ? {} : { label }) });
100
+ }
101
+ return running;
102
+ }
103
+ function hintFor(status, pending, running) {
104
+ switch (status) {
105
+ case 'all_settled':
106
+ return '所有子 Agent 已结束。现在按对账表逐行核对;收齐后先 present_charts(有图)再写完整最终答复。';
107
+ case 'message':
108
+ return `有 ${pending} 条子 Agent 消息待处理,下一步会带上它们;处理完仍有未结算角色就再调用 wait_for_agents。`;
109
+ case 'user_input':
110
+ return `用户在等待期间发来消息;当前有 ${pending} 条可执行消息待处理,先处理用户输入,再决定是否继续等待。`;
111
+ case 'timeout':
112
+ return running > 0
113
+ ? `本次等待窗口内未收敛,仍有 ${running} 个子 Agent 在运行;继续调用 wait_for_agents。running 已列出存活子树,不要再调用 list_agents,也不要 send_message 催运行中的子 Agent;只有用户要求进度或无法继续时才说明。`
114
+ : '本次等待窗口内未收敛,也没有可报告的运行中子 Agent;再调用一次确认,不要调用 list_agents,不要当作完成。';
115
+ }
116
+ }
117
+ export function waitForAgentsToolDefinition(deps) {
118
+ const waitSliceMs = resolveSlice(deps.waitSliceMs);
119
+ const now = deps.now ?? Date.now;
120
+ const parameters = jsonObject({});
121
+ const outputSchema = jsonObject({
122
+ status: { type: 'string', enum: ['all_settled', 'message', 'user_input', 'timeout'] },
123
+ waited_ms: { type: 'integer' },
124
+ pending_messages: { type: 'integer' },
125
+ running: {
126
+ type: 'array',
127
+ items: jsonObject({
128
+ id: { type: 'string' },
129
+ label: { type: 'string' },
130
+ }, ['id']),
131
+ },
132
+ hint: { type: 'string' },
133
+ }, ['status', 'waited_ms', 'pending_messages', 'running', 'hint']);
134
+ return {
135
+ name: 'wait_for_agents',
136
+ description: '等待你的子 Agent 树完成当前工作,单次最多 30 秒;把“每个子 Agent 回传/结算各开一轮”收敛成同一轮。'
137
+ + '创建、复用或 send_message 之后调用它,等待期间不写可见正文,也不要用“请稍候”式正文结束回合。'
138
+ + '返回 message 或 user_input 时,下一步会带上这些消息;处理完继续调用。'
139
+ + '返回 all_settled 才进入对账与最终答复;timeout 表示仍未收敛,仍有 running 时继续调用,不要仅因一次超时写等待行。'
140
+ + '等待期间不要用 list_agents 轮询。只对主 Agent 开放;没有子 Agent 时会立即返回 all_settled。',
141
+ parameters,
142
+ output: { schema: outputSchema, render },
143
+ // Deliberately no isConcurrencySafe: waiting is the ordering barrier between dispatch and synthesis.
144
+ async execute(_args, exec) {
145
+ const agent = exec.agent;
146
+ const sessionId = agent?.session?.id;
147
+ if (agent === undefined || sessionId === undefined) {
148
+ throw new Error('wait_for_agents requires a calling agent (exec.agent was undefined)');
149
+ }
150
+ // `parentSession` alone is a user fork; only the subagent origin marks a delegated child.
151
+ if (agent.session?.header?.origin === 'subagent') {
152
+ throw new Error('wait_for_agents 只对主 Agent 开放:子 Agent 用 send_message 回传结果,不要等待其他 Agent。');
153
+ }
154
+ if (exec.signal.aborted)
155
+ throw abortReason(exec.signal);
156
+ const service = deps.subagents();
157
+ if (service?.waitForChildren === undefined) {
158
+ throw new Error('wait_for_agents 不可用:宿主 subagents 服务未挂载;不要用 list_agents 轮询代替。');
159
+ }
160
+ const started = now();
161
+ const initial = pendingCounts(agent);
162
+ if (initial.users > 0 || initial.children > 0) {
163
+ const status = initial.users > 0 ? 'user_input' : 'message';
164
+ const running = await runningRows(service, deps.agents, sessionId, exec.signal);
165
+ return {
166
+ status,
167
+ waited_ms: Math.max(0, Math.round(now() - started)),
168
+ pending_messages: initial.total,
169
+ running,
170
+ hint: hintFor(status, initial.total, running.length),
171
+ };
172
+ }
173
+ let resolveInbox;
174
+ const inboxEvent = new Promise((resolve) => { resolveInbox = resolve; });
175
+ const onInserted = (...args) => {
176
+ const payload = args[0];
177
+ if (payload?.agent?.session?.id !== sessionId)
178
+ return;
179
+ const kind = sourceKindOf(payload.message);
180
+ if (kind === 'agent-message')
181
+ resolveInbox('message');
182
+ else if (kind === 'user')
183
+ resolveInbox('user_input');
184
+ };
185
+ let dispose;
186
+ if (typeof deps.events?.on === 'function') {
187
+ try {
188
+ dispose = deps.events.on('agent/inbox/inserted', onInserted);
189
+ }
190
+ catch {
191
+ dispose = undefined;
192
+ }
193
+ }
194
+ let outcome;
195
+ const slice = waitTimer(waitSliceMs, exec.signal);
196
+ try {
197
+ const wait = Promise.resolve(service.waitForChildren(agent));
198
+ // The loser of the race must not become an unhandled rejection when the slice or a relay wins.
199
+ wait.catch(() => undefined);
200
+ outcome = await Promise.race([
201
+ wait.then(() => 'settled'),
202
+ inboxEvent,
203
+ slice.promise,
204
+ ]);
205
+ }
206
+ finally {
207
+ slice.dispose();
208
+ if (typeof dispose === 'function')
209
+ dispose();
210
+ }
211
+ const pending = pendingCounts(agent);
212
+ const status = pending.users > 0
213
+ ? 'user_input'
214
+ : pending.children > 0
215
+ ? 'message'
216
+ : outcome === 'timeout' ? 'timeout' : 'all_settled';
217
+ const running = status === 'all_settled'
218
+ ? []
219
+ : await runningRows(service, deps.agents, sessionId, exec.signal);
220
+ return {
221
+ status,
222
+ waited_ms: Math.max(0, Math.round(now() - started)),
223
+ pending_messages: pending.total,
224
+ running,
225
+ hint: hintFor(status, pending.total, running.length),
226
+ };
227
+ },
228
+ };
229
+ }
230
+ export function registerWaitForAgentsTool(ctx, options = {}) {
231
+ const tools = ctx.get('tools');
232
+ if (tools === undefined)
233
+ return;
234
+ const events = ctx;
235
+ const definition = waitForAgentsToolDefinition({
236
+ ...options,
237
+ subagents: () => {
238
+ try {
239
+ return ctx.get('subagents');
240
+ }
241
+ catch {
242
+ return undefined;
243
+ }
244
+ },
245
+ events,
246
+ agents: () => {
247
+ try {
248
+ return ctx.get('agents');
249
+ }
250
+ catch {
251
+ return undefined;
252
+ }
253
+ },
254
+ });
255
+ ctx.effect(() => tools.register(definition), 'capital-generation.tool(wait_for_agents)');
256
+ }
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { Context } from '@deepseek-ai/cordis';
8
8
  import type { RetrieverConfig } from './retriever-config.js';
9
- import type { SelectedSkillSelection } from './agents/selected-skills-provider.js';
9
+ import type { SelectedSkillSelection, SelectedSkillsMode } from './agents/selected-skills-provider.js';
10
10
  export declare const CAPITAL_CONFIG_ENTRY_ID = "capital-config";
11
11
  /** Fields this repo consumes (the host half owns the rest). */
12
12
  export interface CapitalConfigSlice {
@@ -15,7 +15,18 @@ export interface CapitalConfigSlice {
15
15
  customPersona?: string;
16
16
  retriever?: RetrieverConfig;
17
17
  selectedSkills?: SelectedSkillSelection;
18
+ selectedSkillsMode?: SelectedSkillsMode;
18
19
  }
20
+ export type CapitalEntrySource = 'settings-describe' | 'config-editor';
21
+ /**
22
+ * Settings first, then the config-editor save layer. The selected-skill provider and the
23
+ * persona section live in a separate preset row; reading only `settings.describe()` there
24
+ * made a saved toggle look all-off whenever the settings projection was not ready.
25
+ */
26
+ export declare function readCapitalEntry(ctx: Context): {
27
+ value?: Record<string, unknown>;
28
+ source?: CapitalEntrySource;
29
+ };
19
30
  export declare function readCapitalConfig(ctx: Context): CapitalConfigSlice;
20
31
  /** Flat credential refs needed by the collector domain (same shape as `CollectorConfig`). */
21
32
  export declare function readCollectorConfig(ctx: Context): {
@@ -1,12 +1,32 @@
1
1
  export const CAPITAL_CONFIG_ENTRY_ID = 'capital-config';
2
- export function readCapitalConfig(ctx) {
2
+ /**
3
+ * Settings first, then the config-editor save layer. The selected-skill provider and the
4
+ * persona section live in a separate preset row; reading only `settings.describe()` there
5
+ * made a saved toggle look all-off whenever the settings projection was not ready.
6
+ */
7
+ export function readCapitalEntry(ctx) {
3
8
  try {
4
9
  const settings = ctx.get('settings');
5
- return settings?.describe?.().find((entry) => entry?.ns === CAPITAL_CONFIG_ENTRY_ID)?.value ?? {};
10
+ const value = settings?.describe?.().find((entry) => entry?.ns === CAPITAL_CONFIG_ENTRY_ID)?.value;
11
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
12
+ return { value: value, source: 'settings-describe' };
13
+ }
6
14
  }
7
15
  catch {
8
- return {};
9
16
  }
17
+ try {
18
+ const editor = ctx.get('configEditor');
19
+ const value = editor?.entries?.().find((row) => row?.options?.id === CAPITAL_CONFIG_ENTRY_ID)?.options?.config;
20
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
21
+ return { value: value, source: 'config-editor' };
22
+ }
23
+ }
24
+ catch {
25
+ }
26
+ return {};
27
+ }
28
+ export function readCapitalConfig(ctx) {
29
+ return (readCapitalEntry(ctx).value ?? {});
10
30
  }
11
31
  /** Flat credential refs needed by the collector domain (same shape as `CollectorConfig`). */
12
32
  export function readCollectorConfig(ctx) {
@@ -11,6 +11,8 @@ export interface ChartArtifactRef {
11
11
  dataset_id: string | null;
12
12
  source_label: string | null;
13
13
  captured_at: number | null;
14
+ /** Six-digit security/index code parsed from the Dataset envelope or rows, when present. */
15
+ subject_code: string | null;
14
16
  kind: string;
15
17
  axis: 'time' | 'index';
16
18
  title: string;
@@ -28,7 +30,7 @@ export interface ChartArtifactRegistryOptions {
28
30
  }
29
31
  /**
30
32
  * In-process chart_ref capabilities: session/task/workspace binding and the deliverable owner
31
- * scope. Process restart invalidates them; chart.html remains the offline fallback.
33
+ * scope. Process restart invalidates them; the title-named self-contained HTML remains the offline fallback.
32
34
  */
33
35
  export declare class ChartArtifactRegistry {
34
36
  private readonly records;
@@ -44,6 +46,7 @@ export declare class ChartArtifactRegistry {
44
46
  dataset_id?: string | null;
45
47
  source_label?: string | null;
46
48
  captured_at?: number | null;
49
+ subject_code?: string | null;
47
50
  kind: string;
48
51
  axis: 'time' | 'index';
49
52
  title: string;
@@ -18,7 +18,7 @@ export function chartSessionScopeId(session) {
18
18
  }
19
19
  /**
20
20
  * In-process chart_ref capabilities: session/task/workspace binding and the deliverable owner
21
- * scope. Process restart invalidates them; chart.html remains the offline fallback.
21
+ * scope. Process restart invalidates them; the title-named self-contained HTML remains the offline fallback.
22
22
  */
23
23
  export class ChartArtifactRegistry {
24
24
  records = new Map();
@@ -57,6 +57,7 @@ export class ChartArtifactRegistry {
57
57
  dataset_id: input.dataset_id ?? null,
58
58
  source_label: input.source_label ?? null,
59
59
  captured_at: input.captured_at ?? null,
60
+ subject_code: input.subject_code ?? null,
60
61
  kind: input.kind,
61
62
  axis: input.axis,
62
63
  title: input.title,
@@ -31,6 +31,15 @@ export interface AssistantStreamFrameLike {
31
31
  };
32
32
  };
33
33
  }
34
+ /** Live root Agent passed to the closing hook so it can cancel inbox notices before the loop claims them. */
35
+ export interface DeliveryAgentLike {
36
+ readonly session?: SessionLike;
37
+ readonly inbox?: {
38
+ readonly nextTurn?: readonly unknown[];
39
+ readonly nextStep?: readonly unknown[];
40
+ remove?(messageId: string): boolean;
41
+ };
42
+ }
34
43
  interface InboxStateLike {
35
44
  'next-turn'?: readonly unknown[];
36
45
  'next-step'?: readonly unknown[];
@@ -41,13 +50,18 @@ export interface ChartDeliveryOptions {
41
50
  publish: (sessionId: string, inputs: ChartTurnEventInput[]) => boolean;
42
51
  /** Durable pending input; synchronous projection read. Absent means "cannot tell" → proceed. */
43
52
  inboxOf?: (session: SessionLike) => InboxStateLike | undefined;
53
+ /**
54
+ * Runs on every closing finish, including exchanges with no chart. The wake guard removes
55
+ * redundant settlement notices here; it must never be allowed to block delivery.
56
+ */
57
+ onClosing?: (session: SessionLike, agent: DeliveryAgentLike | undefined, turn: number | undefined) => void;
44
58
  /** Bounded batch; the card collapses past four files and expands on demand. */
45
59
  maxCharts?: number;
46
60
  }
47
61
  export interface ChartDelivery {
48
62
  /** `present_charts` selection; merged across calls because models often retry in batches. */
49
63
  request(session: SessionLike, inputs: ChartTurnEventInput[]): void;
50
- acceptStream(session: SessionLike | undefined, frame: AssistantStreamFrameLike): void;
64
+ acceptStream(session: SessionLike | undefined, frame: AssistantStreamFrameLike, agent?: DeliveryAgentLike): void;
51
65
  /** A new human message starts a new exchange: old charts must not ride into it. */
52
66
  acceptUserMessage(sessionId: string, time: number): void;
53
67
  acceptSubagentStart(info: {
@@ -14,7 +14,8 @@ export function createChartDelivery(options) {
14
14
  let explicit = [];
15
15
  const exchangeStarts = new Map();
16
16
  const fallbackStart = Date.now();
17
- let lastFlushedTurn;
17
+ /** One card per exchange. `turn` is unreliable: a later finish in the same turn may omit it. */
18
+ let flushed = false;
18
19
  let sawFrame = false;
19
20
  const attempts = new Map();
20
21
  const running = new Map();
@@ -54,14 +55,22 @@ export function createChartDelivery(options) {
54
55
  return true;
55
56
  return inboxMessages(inbox).every(isSettledNotice);
56
57
  };
57
- const flush = (session, turn) => {
58
+ const flush = (session, turn, agent) => {
58
59
  // A later step in the same exchange may omit `turn`; it still must not write a second card.
59
- if (lastFlushedTurn !== undefined && (turn === undefined || lastFlushedTurn === turn))
60
+ if (flushed)
60
61
  return;
61
62
  const inbox = options.inboxOf?.(session);
62
63
  reconcileSettled(inbox);
63
64
  if (!idle(inbox))
64
65
  return;
66
+ // The wake guard drops notices that would otherwise make the loop open another step after
67
+ // this answer. Run it even when no chart is selected, and never let it block delivery.
68
+ try {
69
+ options.onClosing?.(session, agent, turn);
70
+ }
71
+ catch {
72
+ // A guard failure must not strand an already-selected card.
73
+ }
65
74
  const inputs = (explicit.length > 0 ? explicit : autoInputs(session)).slice(0, maxCharts);
66
75
  if (inputs.length === 0)
67
76
  return;
@@ -72,9 +81,9 @@ export function createChartDelivery(options) {
72
81
  catch {
73
82
  written = false;
74
83
  }
75
- // Sentinel keeps a second turn-less finish in the same exchange from duplicating the card.
84
+ // A failed write keeps the exchange open so a later closing finish can retry.
76
85
  if (written)
77
- lastFlushedTurn = turn ?? -1;
86
+ flushed = true;
78
87
  };
79
88
  return {
80
89
  request(session, inputs) {
@@ -90,7 +99,7 @@ export function createChartDelivery(options) {
90
99
  if (explicit.length > maxCharts)
91
100
  explicit = explicit.slice(0, maxCharts);
92
101
  },
93
- acceptStream(session, frame) {
102
+ acceptStream(session, frame, agent) {
94
103
  if (!owns(session) || frame === undefined)
95
104
  return;
96
105
  sawFrame = true;
@@ -116,14 +125,14 @@ export function createChartDelivery(options) {
116
125
  }
117
126
  if (chunk.type !== 'finish' || chunk.reason?.kind !== 'stop' || attempt.toolCall)
118
127
  return;
119
- flush(session, frame.turn);
128
+ flush(session, frame.turn, agent);
120
129
  },
121
130
  acceptUserMessage(id, time) {
122
131
  exchangeStarts.set(id, time);
123
132
  if (id !== sessionId)
124
133
  return;
125
134
  explicit = [];
126
- lastFlushedTurn = undefined;
135
+ flushed = false;
127
136
  },
128
137
  acceptSubagentStart(info) {
129
138
  if (typeof info?.id !== 'string' || info.id.length === 0)
@@ -33,7 +33,7 @@ export interface ChartTurnEventInput {
33
33
  ownerSessionId: string;
34
34
  chart_id: string;
35
35
  title: string;
36
- /** Workspace-relative chart.html path; the official preview resolves it against session.cwd. */
36
+ /** Workspace-relative self-contained HTML path; the official preview resolves it against session.cwd. */
37
37
  html_path: string;
38
38
  }
39
39
  export interface ChartEventPublisher {