mixdog 1.3.0 → 1.3.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/NOTICE.md +52 -9
- package/package.json +1 -1
- package/src/defaults/shipped-history.json +4 -1
- package/src/defaults/skills/setup/references/actions.md +4 -1
- package/src/defaults/skills/setup-guide/SKILL.md +10 -7
- package/src/output-styles/common.md +13 -8
- package/src/rules/agent-mode/AGENT_MODE.md +1 -1
- package/src/rules/agent-mode/MIXY.md +11 -4
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +72 -14
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -2
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +14 -4
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +37 -7
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +4 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +17 -8
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +32 -5
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +8 -14
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +46 -4
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +7 -20
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +17 -1
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +61 -9
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +14 -9
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
- package/src/runtime/attachments/store.mjs +6 -0
- package/src/runtime/effort-judge/judge-client.mjs +1 -1
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/shared/agent-kind.mjs +152 -0
- package/src/runtime/shared/agent-route-config.d.mts +1 -0
- package/src/runtime/shared/agent-route-config.mjs +22 -8
- package/src/runtime/shared/agent-store.mjs +132 -0
- package/src/runtime/shared/bot-split-migration.mjs +457 -0
- package/src/runtime/shared/config-patch.mjs +20 -1
- package/src/runtime/shared/connector-catalog.d.mts +1 -2
- package/src/runtime/shared/connector-catalog.mjs +2 -4
- package/src/runtime/shared/connector-triggers.mjs +2 -0
- package/src/runtime/shared/err-text.mjs +3 -1
- package/src/runtime/shared/error-presentation.mjs +91 -6
- package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
- package/src/runtime/shared/handoff-artifacts.mjs +61 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
- package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
- package/src/runtime/shared/model-route-fallback.mjs +79 -7
- package/src/runtime/shared/plain-preview.mjs +22 -17
- package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
- package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
- package/src/runtime/shared/usage-readout-totals.mjs +15 -13
- package/src/runtime/shared/user-data-guard.mjs +5 -2
- package/src/session-runtime/agent-home-delivery.mjs +423 -56
- package/src/session-runtime/agent-room-delivery.mjs +45 -4
- package/src/session-runtime/agent-task-sessions.mjs +299 -39
- package/src/session-runtime/boot/apis.mjs +13 -0
- package/src/session-runtime/boot/providers.mjs +14 -4
- package/src/session-runtime/config-lifecycle.mjs +43 -2
- package/src/session-runtime/handoff-attachments.mjs +593 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +27 -3
- package/src/session-runtime/model-route/route-persist.mjs +2 -2
- package/src/session-runtime/model-route/route-tuning.mjs +7 -1
- package/src/session-runtime/model-route/set-route.mjs +21 -1
- package/src/session-runtime/native-web-search.mjs +2 -2
- package/src/session-runtime/new-session-config.mjs +6 -1
- package/src/session-runtime/route-resolution.mjs +5 -11
- package/src/session-runtime/services/agent-tool/helpers.mjs +24 -5
- package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
- package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
- package/src/session-runtime/setup-tool/executor.mjs +12 -1
- package/src/session-runtime/setup-tool/extended-actions.mjs +19 -7
- package/src/session-runtime/setup-tool/settings-contract.mjs +6 -0
- package/src/session-runtime/tool-policy-refresh.mjs +13 -2
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +11 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +123 -32
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
- package/src/standalone/daemon-session-control.mjs +13 -5
- package/src/standalone/daemon.mjs +16 -4
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
- package/src/standalone/session-service/agent-tree.mjs +4 -3
- package/src/standalone/session-service/session-calls/session-turns.mjs +19 -0
- package/src/tui/dist/index.mjs +70 -9
- package/src/tui/session/notice-surface.mjs +3 -4
- package/src/tui/session/session-api/routes.mjs +14 -3
- package/src/tui/session/transcript-intake.mjs +2 -0
- package/src/tui/session/turn-live-call.mjs +11 -3
- package/src/tui/session/turn.mjs +13 -3
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
import {
|
|
11
11
|
normalizeRoomRow,
|
|
12
12
|
roomIdOfSession,
|
|
13
|
+
roomMemberOfSession,
|
|
13
14
|
roomRowItem,
|
|
14
15
|
roomRowMessage,
|
|
15
16
|
} from '../runtime/agent/orchestrator/session/agent-room.mjs';
|
|
17
|
+
import { artifactRecords } from '../runtime/shared/handoff-artifacts.mjs';
|
|
18
|
+
import { collectTurnArtifacts, sessionRoots, turnSlice } from './handoff-attachments.mjs';
|
|
16
19
|
import {
|
|
17
20
|
attachmentsForIntake,
|
|
18
21
|
isAttachmentReference,
|
|
@@ -46,14 +49,52 @@ async function storedRow(row) {
|
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
/** A member's speech with the files its turn produced taken in: `artifactsFrom`
|
|
53
|
+
* `{ sessionId, since }` names the member's own session in this room and when
|
|
54
|
+
* its turn started; the files inside that session's folders are stored and
|
|
55
|
+
* ride on the row as references (handoff-attachments.mjs). */
|
|
56
|
+
function speechWithArtifacts(row, roomId, loadSession, log) {
|
|
57
|
+
const from =
|
|
58
|
+
row?.kind === 'speech' && row.artifactsFrom && typeof row.artifactsFrom === 'object' ? row.artifactsFrom : null;
|
|
59
|
+
if (!from) return row;
|
|
60
|
+
const { artifactsFrom: _from, ...rest } = row;
|
|
61
|
+
const member = roomMemberOfSession(String(from.sessionId || ''));
|
|
62
|
+
if (!member || member.roomId !== roomId || member.agentId !== row.speaker) return rest;
|
|
63
|
+
try {
|
|
64
|
+
const session = loadSession(from.sessionId);
|
|
65
|
+
const messages = turnSlice(session?.messages, { since: Number(from.since) || 0 });
|
|
66
|
+
const { files } = collectTurnArtifacts(messages, { cwd: session?.cwd, roots: sessionRoots(session) });
|
|
67
|
+
const stored = files.filter((file) => file.id);
|
|
68
|
+
if (!stored.length) return rest;
|
|
69
|
+
return {
|
|
70
|
+
...rest,
|
|
71
|
+
parts: stored.map((file) => ({
|
|
72
|
+
type: file.kind === 'image' ? 'image' : file.kind === 'text' ? 'text' : 'file',
|
|
73
|
+
attachmentRef: file.id,
|
|
74
|
+
...(file.kind === 'text' ? { textKind: 'file' } : {}),
|
|
75
|
+
mimeType: file.mimeType,
|
|
76
|
+
filename: file.name,
|
|
77
|
+
sizeBytes: file.size,
|
|
78
|
+
})),
|
|
79
|
+
attachments: artifactRecords(stored),
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
log(`the files of ${from.sessionId} could not be taken in: ${error?.message || error}`);
|
|
83
|
+
return rest;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
49
87
|
export function createAgentRoomDelivery({ loadSession, saveSession, live = agentHomeLiveSurface, log = () => {} }) {
|
|
50
88
|
/** Append `rows` to the log of `sessionId`; resolves `{ added, rows }`: how
|
|
51
89
|
* many were new, and every row as stored (files as references, no bytes). */
|
|
52
90
|
async function append(sessionId, rows) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
91
|
+
const roomId = roomIdOfSession(sessionId);
|
|
92
|
+
if (!roomId) throw new TypeError('not a group-room session');
|
|
93
|
+
const kept = (
|
|
94
|
+
await Promise.all(
|
|
95
|
+
(Array.isArray(rows) ? rows : []).map((row) => storedRow(speechWithArtifacts(row, roomId, loadSession, log)))
|
|
96
|
+
)
|
|
97
|
+
).map((row) => normalizeRoomRow(row));
|
|
57
98
|
const messages = kept.map((row) => roomRowMessage(row));
|
|
58
99
|
const session = loadSession(sessionId);
|
|
59
100
|
if (!session || session.closed === true) throw new Error(`session ${sessionId} is not available`);
|
|
@@ -9,11 +9,19 @@
|
|
|
9
9
|
* home (agent-home-delivery.mjs), which posts it and wakes the agent once to
|
|
10
10
|
* relay it. A turn blocked on the user's answer or approval posts one faint
|
|
11
11
|
* line instead; the card stays in the task session for the user.
|
|
12
|
+
*
|
|
13
|
+
* An agent's send to any other session reports the same way, by an origin for
|
|
14
|
+
* that send only (reportOrigin). Every reporting turn is also watched durably
|
|
15
|
+
* (agent-home-delivery.mjs `watches`) until its end is queued, so a daemon
|
|
16
|
+
* restart before that re-attaches it (recover): an ended turn reports once
|
|
17
|
+
* from the target's record, a queued or running one is watched to its end.
|
|
12
18
|
*/
|
|
13
19
|
import { randomUUID } from 'node:crypto';
|
|
14
20
|
import { realpathSync, statSync } from 'node:fs';
|
|
15
21
|
import { basename, resolve } from 'node:path';
|
|
16
22
|
import { clean } from '../runtime/agent/orchestrator/runtime-core/session-text.mjs';
|
|
23
|
+
import { collectTurnArtifacts, handoffContent, sessionRoots, turnSlice } from './handoff-attachments.mjs';
|
|
24
|
+
import { roomMemberOfSession } from '../runtime/agent/orchestrator/session/agent-room.mjs';
|
|
17
25
|
|
|
18
26
|
const TITLE_LIMIT = 60;
|
|
19
27
|
|
|
@@ -51,9 +59,72 @@ export function resolveTaskProject(project, { projects = [], cwd = process.cwd()
|
|
|
51
59
|
const turnStatus = (detail) =>
|
|
52
60
|
({ done: 'completed', completed: 'completed', failed: 'failed' })[detail?.status] || 'cancelled';
|
|
53
61
|
|
|
62
|
+
const INTERRUPTED = /^\[Request interrupted/;
|
|
63
|
+
|
|
64
|
+
const messageText = (content) =>
|
|
65
|
+
typeof content === 'string'
|
|
66
|
+
? content
|
|
67
|
+
: (Array.isArray(content) ? content : [])
|
|
68
|
+
.map((part) => (typeof part === 'string' ? part : part?.type === 'text' ? part.text || '' : ''))
|
|
69
|
+
.join('');
|
|
70
|
+
|
|
71
|
+
/** The submissions a stored user message delivered: a turn's prompt
|
|
72
|
+
* (`checkpointId`, `submissionIds` of a merged batch) or a steered message. */
|
|
73
|
+
export function submissionIdsOf(message) {
|
|
74
|
+
if (message?.role !== 'user') return [];
|
|
75
|
+
const meta = message.meta || {};
|
|
76
|
+
const transcript = meta.transcript || {};
|
|
77
|
+
return [
|
|
78
|
+
transcript.checkpointId,
|
|
79
|
+
...(Array.isArray(transcript.submissionIds) ? transcript.submissionIds : []),
|
|
80
|
+
...(Array.isArray(meta.submissionIds) ? meta.submissionIds : []),
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The end of the turn that took submission `runId`, from a session's stored
|
|
85
|
+
* `messages`: `{ status, text }` once that turn recorded its end (its
|
|
86
|
+
* completion, an interruption marker, or the next turn's prompt), 'open'
|
|
87
|
+
* while it has none, null when no turn took it. */
|
|
88
|
+
export function submissionOutcome(messages, runId) {
|
|
89
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
90
|
+
const start = list.findIndex((message) => submissionIdsOf(message).includes(runId));
|
|
91
|
+
if (start < 0) return null;
|
|
92
|
+
for (let index = start; index < list.length; index += 1) {
|
|
93
|
+
const message = list[index];
|
|
94
|
+
const completion = message?.meta?.transcript?.completion;
|
|
95
|
+
if (completion) {
|
|
96
|
+
const status = turnStatus(completion);
|
|
97
|
+
const text = status === 'failed' ? completion.detail || '' : message.role === 'assistant' ? message.content : '';
|
|
98
|
+
return { status, text: messageText(text) };
|
|
99
|
+
}
|
|
100
|
+
if (index === start || message?.role !== 'user') continue;
|
|
101
|
+
if (INTERRUPTED.test(messageText(message.content)) || message.meta?.transcript?.checkpointId) {
|
|
102
|
+
return { status: 'cancelled', text: '' };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return 'open';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const LOST_TEXT = 'The message never ran: the app restarted before its turn started.';
|
|
109
|
+
const GONE_TEXT = 'The session no longer exists.';
|
|
110
|
+
|
|
111
|
+
// The last-used model search stops after this many catalog pages.
|
|
112
|
+
const LAST_USED_ROUTE_MAX_PAGES = 20;
|
|
113
|
+
|
|
54
114
|
export function createAgentTaskSessions({
|
|
55
115
|
getSessionService,
|
|
56
116
|
deliver = async (agentId, report) => (await import('./agent-home-delivery.mjs')).deliverAgentTask(agentId, report),
|
|
117
|
+
// The durable watch of each reporting turn until its end is queued.
|
|
118
|
+
watches = {
|
|
119
|
+
add: async (watch) => (await import('./agent-home-delivery.mjs')).watchAgentTask(watch),
|
|
120
|
+
list: async () => (await import('./agent-home-delivery.mjs')).agentTaskWatches(),
|
|
121
|
+
remove: async (runId) => (await import('./agent-home-delivery.mjs')).unwatchAgentTask(runId),
|
|
122
|
+
},
|
|
123
|
+
// Submissions still queued (durably) for a session after a restart.
|
|
124
|
+
queuedSubmissionIds = async (sessionId) =>
|
|
125
|
+
(await (await import('../tui/session/tui-steering-persist.mjs')).readTuiSteeringPersist(sessionId))
|
|
126
|
+
.map((row) => row?.submissionId)
|
|
127
|
+
.filter(Boolean),
|
|
57
128
|
loadSession = async (sessionId) =>
|
|
58
129
|
(await import('../runtime/agent/orchestrator/session/store.mjs')).loadSession(sessionId),
|
|
59
130
|
listProjects = async () => (await import('../runtime/shared/projects.mjs')).listProjects(),
|
|
@@ -73,8 +144,56 @@ export function createAgentTaskSessions({
|
|
|
73
144
|
if (!preset) throw new Error(`model "${ref}" is neither a provider/model route nor a known preset name`);
|
|
74
145
|
return modelRouteFields(preset);
|
|
75
146
|
},
|
|
147
|
+
// The Lead (Main) route of the config — what a desktop New task starts on.
|
|
148
|
+
// Null while none is chosen.
|
|
149
|
+
leadRoute = async () => {
|
|
150
|
+
const [{ modelRouteFields }, { resolveRoute }, { loadConfig }] = await Promise.all([
|
|
151
|
+
import('../runtime/shared/schedule-model-ref.mjs'),
|
|
152
|
+
import('./boot/shared.mjs'),
|
|
153
|
+
import('../runtime/agent/orchestrator/config.mjs'),
|
|
154
|
+
]);
|
|
155
|
+
const route = resolveRoute(loadConfig({ secrets: false }), {});
|
|
156
|
+
return clean(route?.provider) && clean(route?.model) ? modelRouteFields(route) : null;
|
|
157
|
+
},
|
|
158
|
+
// The model the user last used in a coding session (the runtime's stand-in
|
|
159
|
+
// for the desktop's last New-task model): the first row that has one in the
|
|
160
|
+
// catalog, which lists newest activity first. Pages on (bounded) until a
|
|
161
|
+
// row qualifies; null when none does.
|
|
162
|
+
lastUsedRoute = async (sessionService) => {
|
|
163
|
+
let cursor = null;
|
|
164
|
+
for (let page = 0; page < LAST_USED_ROUTE_MAX_PAGES; page += 1) {
|
|
165
|
+
const listed = await sessionService.listSessionCatalog({ page: { limit: 50, cursor } });
|
|
166
|
+
const used = (Array.isArray(listed?.sessions) ? listed.sessions : []).find(
|
|
167
|
+
(row) => row?.sessionMode !== 'agent' && clean(row?.provider) && clean(row?.model)
|
|
168
|
+
);
|
|
169
|
+
if (used) return { provider: clean(used.provider), model: clean(used.model) };
|
|
170
|
+
cursor = listed?.nextCursor || null;
|
|
171
|
+
if (!cursor) return null;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
},
|
|
175
|
+
// The files turn `runId` of `sessionId` produced (handoff-attachments.mjs),
|
|
176
|
+
// read from its live runtime when one is given, else its stored record.
|
|
177
|
+
turnArtifacts = async (sessionId, runId, runtime = null) => {
|
|
178
|
+
const stored = await loadSession(sessionId);
|
|
179
|
+
const live = typeof runtime?.readModelMessages === 'function' ? await runtime.readModelMessages(0) : null;
|
|
180
|
+
const messages = turnSlice(Array.isArray(live?.messages) ? live.messages : stored?.messages, { runId });
|
|
181
|
+
return collectTurnArtifacts(messages, { cwd: stored?.cwd, roots: sessionRoots(stored) });
|
|
182
|
+
},
|
|
76
183
|
log = (line) => process.stderr.write(`mixdog task sessions: ${line}\n`),
|
|
77
184
|
}) {
|
|
185
|
+
/** The report fields of turn `runId`'s files; none when it produced none. */
|
|
186
|
+
const artifactFields = async (sessionId, runId, runtime = null) => {
|
|
187
|
+
try {
|
|
188
|
+
const { files, omitted } = await turnArtifacts(sessionId, runId, runtime);
|
|
189
|
+
return files.length ? { files, ...(omitted ? { omitted } : {}) } : {};
|
|
190
|
+
} catch (error) {
|
|
191
|
+
log(`collecting the files of ${sessionId} failed: ${error?.message || error}`);
|
|
192
|
+
return {};
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
// Marks this process's watches: recovery re-attaches only an earlier one's.
|
|
196
|
+
const processToken = randomUUID();
|
|
78
197
|
const service = () => {
|
|
79
198
|
const value = getSessionService();
|
|
80
199
|
if (!value) throw new Error('session: session service is unavailable');
|
|
@@ -85,53 +204,165 @@ export function createAgentTaskSessions({
|
|
|
85
204
|
* user in it) reports to the agent's home. Resolves on intake. */
|
|
86
205
|
async function agentTurn(sessionId, prompt, origin, id = `atask_${randomUUID()}`, beforeReport = null) {
|
|
87
206
|
const sessions = service();
|
|
207
|
+
const target = origin.target === 'session' ? { target: 'session' } : {};
|
|
208
|
+
// The watch is durable before the turn can start, so no end goes
|
|
209
|
+
// unreported across a restart; a turn whose watch could not be stored
|
|
210
|
+
// runs without a report (reportsBack false) rather than claim one.
|
|
211
|
+
let notify = false;
|
|
212
|
+
if (origin.notify !== false) {
|
|
213
|
+
try {
|
|
214
|
+
await watches.add({
|
|
215
|
+
runId: id,
|
|
216
|
+
agentId: origin.agentId,
|
|
217
|
+
homeSessionId: origin.sessionId,
|
|
218
|
+
taskSessionId: sessionId,
|
|
219
|
+
title: origin.title,
|
|
220
|
+
...target,
|
|
221
|
+
...(origin.textOnly ? { textOnly: true } : {}),
|
|
222
|
+
owner: processToken,
|
|
223
|
+
at: Date.now(),
|
|
224
|
+
});
|
|
225
|
+
notify = true;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
log(`watch of ${sessionId} failed, sending without a report: ${error?.message || error}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const dropWatch = () =>
|
|
231
|
+
watches.remove(id).catch((error) => log(`dropping the watch of ${sessionId} failed: ${error?.message || error}`));
|
|
232
|
+
let runtime = null;
|
|
88
233
|
const report = (status, text = '') =>
|
|
89
234
|
Promise.resolve(beforeReport)
|
|
90
|
-
.then(() =>
|
|
235
|
+
.then(async () =>
|
|
91
236
|
deliver(origin.agentId, {
|
|
92
237
|
runId: `${id}_${status === 'blocked' ? 'blocked' : 'end'}`,
|
|
93
238
|
taskSessionId: sessionId,
|
|
94
239
|
title: origin.title,
|
|
95
240
|
status,
|
|
96
241
|
text,
|
|
242
|
+
...target,
|
|
243
|
+
// A turn's end brings back the files it produced, when the agent
|
|
244
|
+
// may receive them (reportOrigin `textOnly`).
|
|
245
|
+
...(status === 'blocked'
|
|
246
|
+
? {}
|
|
247
|
+
: { watchId: id, ...(origin.textOnly ? {} : await artifactFields(sessionId, id, runtime)) }),
|
|
97
248
|
})
|
|
98
249
|
)
|
|
99
250
|
.catch((error) => log(`report of ${sessionId} failed: ${error?.message || error}`));
|
|
100
|
-
const notify = origin.notify !== false;
|
|
101
251
|
let unwatch = () => {};
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
252
|
+
let result;
|
|
253
|
+
try {
|
|
254
|
+
if (notify) {
|
|
255
|
+
runtime = await sessions.materializeSession(sessionId);
|
|
256
|
+
let blocked = false;
|
|
257
|
+
unwatch =
|
|
258
|
+
runtime?.subscribe?.(() => {
|
|
259
|
+
if (blocked || !runtime.getState?.()?.toolApproval) return;
|
|
260
|
+
blocked = true;
|
|
261
|
+
void report('blocked');
|
|
262
|
+
}) || unwatch;
|
|
263
|
+
}
|
|
264
|
+
result = await sessions.submitSession({
|
|
265
|
+
sessionId,
|
|
266
|
+
prompt,
|
|
267
|
+
options: {
|
|
268
|
+
id,
|
|
269
|
+
mode: 'prompt',
|
|
270
|
+
transcriptMeta: { sender: 'lead' },
|
|
271
|
+
...(notify
|
|
272
|
+
? {
|
|
273
|
+
onSettled: (detail) => {
|
|
274
|
+
unwatch();
|
|
275
|
+
void report(turnStatus(detail), detail?.result?.content || detail?.error || '');
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
: {}),
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
} catch (error) {
|
|
282
|
+
unwatch();
|
|
283
|
+
if (notify) await dropWatch();
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
const accepted = result?.accepted === true;
|
|
287
|
+
if (!accepted) {
|
|
288
|
+
unwatch();
|
|
289
|
+
if (notify) await dropWatch();
|
|
111
290
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
291
|
+
return { accepted, reportsBack: accepted && notify };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Re-attach one watch a stopped daemon left: report the turn's end once
|
|
295
|
+
* from the target's record, or watch it until it ends. */
|
|
296
|
+
async function rearm(watch) {
|
|
297
|
+
const sessionId = watch.taskSessionId;
|
|
298
|
+
const finish = async (status, text = '') =>
|
|
299
|
+
deliver(watch.agentId, {
|
|
300
|
+
runId: `${watch.runId}_end`,
|
|
301
|
+
taskSessionId: sessionId,
|
|
302
|
+
title: watch.title,
|
|
303
|
+
status,
|
|
304
|
+
text,
|
|
305
|
+
...(watch.target === 'session' ? { target: 'session' } : {}),
|
|
306
|
+
watchId: watch.runId,
|
|
307
|
+
...(watch.textOnly || text === GONE_TEXT || text === LOST_TEXT
|
|
308
|
+
? {}
|
|
309
|
+
: await artifactFields(sessionId, watch.runId)),
|
|
310
|
+
});
|
|
311
|
+
if (!(await loadSession(sessionId))) return finish('cancelled', GONE_TEXT);
|
|
312
|
+
const runtime = await service().materializeSession(sessionId);
|
|
313
|
+
let done = false;
|
|
314
|
+
let checking = false;
|
|
315
|
+
let again = false;
|
|
316
|
+
let unsubscribe = () => {};
|
|
317
|
+
const check = async () => {
|
|
318
|
+
if (done) return;
|
|
319
|
+
if (checking) {
|
|
320
|
+
again = true;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
checking = true;
|
|
324
|
+
try {
|
|
325
|
+
do {
|
|
326
|
+
again = false;
|
|
327
|
+
const outcome = submissionOutcome((await loadSession(sessionId))?.messages, watch.runId);
|
|
328
|
+
const state = runtime?.getState?.() || {};
|
|
329
|
+
let end = outcome && outcome !== 'open' ? outcome : null;
|
|
330
|
+
if (!end && !state.busy) {
|
|
331
|
+
const queued = (state.queued || []).some((entry) => entry?.id === watch.runId);
|
|
332
|
+
if (outcome === 'open' && !queued) end = { status: 'cancelled', text: '' };
|
|
333
|
+
else if (!outcome && !queued && !(await queuedSubmissionIds(sessionId)).includes(watch.runId)) {
|
|
334
|
+
end = { status: 'cancelled', text: LOST_TEXT };
|
|
125
335
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
336
|
+
}
|
|
337
|
+
if (end) {
|
|
338
|
+
done = true;
|
|
339
|
+
unsubscribe();
|
|
340
|
+
await finish(end.status, end.text);
|
|
341
|
+
}
|
|
342
|
+
} while (again && !done);
|
|
343
|
+
} finally {
|
|
344
|
+
checking = false;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const failed = (error) => log(`re-attaching ${sessionId} failed: ${error?.message || error}`);
|
|
348
|
+
unsubscribe = runtime?.subscribe?.(() => void check().catch(failed)) || unsubscribe;
|
|
349
|
+
await check();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Daemon start: re-attach every reporting turn an earlier process watched.
|
|
353
|
+
* Resolves once each is reported or watched. */
|
|
354
|
+
async function recover() {
|
|
355
|
+
const pending = (await watches.list()).filter((watch) => watch?.owner !== processToken);
|
|
356
|
+
await Promise.all(
|
|
357
|
+
pending.map((watch) =>
|
|
358
|
+
rearm(watch).catch((error) => log(`re-attaching ${watch?.taskSessionId} failed: ${error?.message || error}`))
|
|
359
|
+
)
|
|
360
|
+
);
|
|
361
|
+
return pending.length;
|
|
131
362
|
}
|
|
132
363
|
|
|
133
364
|
/** Create the task session for `brief` and start it; returns at once. */
|
|
134
|
-
async function create(args = {}, { callerSessionId, callerAgentId, callerCwd } = {}) {
|
|
365
|
+
async function create(args = {}, { callerSessionId, callerAgentId, callerCwd, attachmentParts = [] } = {}) {
|
|
135
366
|
if (!callerAgentId) throw new Error('session: create is only for an agent conversation');
|
|
136
367
|
const brief = typeof args.brief === 'string' ? args.brief.trim() : '';
|
|
137
368
|
if (!brief) throw new Error('session: brief is required for action=create');
|
|
@@ -145,7 +376,13 @@ export function createAgentTaskSessions({
|
|
|
145
376
|
title,
|
|
146
377
|
notify: args.notify !== false,
|
|
147
378
|
};
|
|
148
|
-
|
|
379
|
+
// A task session runs in coding mode under Lead: an explicit model wins,
|
|
380
|
+
// else the Lead route, else the last-used model. Never a model-less
|
|
381
|
+
// session — the composer would show no model and its first turn would fail.
|
|
382
|
+
const route = clean(args.model)
|
|
383
|
+
? await modelRoute(clean(args.model))
|
|
384
|
+
: (await leadRoute()) || (await lastUsedRoute(service()));
|
|
385
|
+
if (!route) throw new Error('session: no model to start the task session on. Choose a Lead model first.');
|
|
149
386
|
const sessions = service();
|
|
150
387
|
const created = await sessions.createSession({
|
|
151
388
|
...route,
|
|
@@ -175,9 +412,11 @@ export function createAgentTaskSessions({
|
|
|
175
412
|
const ready = new Promise((resolveReady) => {
|
|
176
413
|
started = resolveReady;
|
|
177
414
|
});
|
|
178
|
-
|
|
415
|
+
// The brief carries the files the agent forwarded (workspace-tools.mjs).
|
|
416
|
+
const prompt = handoffContent(brief, attachmentParts);
|
|
417
|
+
const { accepted, reportsBack } = await agentTurn(sessionId, prompt, taskOrigin, requestId, ready);
|
|
179
418
|
if (!accepted) throw new Error(`session: task session ${sessionId} did not accept the brief`);
|
|
180
|
-
if (
|
|
419
|
+
if (reportsBack) {
|
|
181
420
|
await deliver(callerAgentId, {
|
|
182
421
|
runId: `${requestId}_started`,
|
|
183
422
|
taskSessionId: sessionId,
|
|
@@ -186,15 +425,36 @@ export function createAgentTaskSessions({
|
|
|
186
425
|
}).catch((error) => log(`start line of ${sessionId} failed: ${error?.message || error}`));
|
|
187
426
|
}
|
|
188
427
|
started();
|
|
189
|
-
return {
|
|
428
|
+
return {
|
|
429
|
+
session_id: sessionId,
|
|
430
|
+
title,
|
|
431
|
+
project: where.project?.path ?? null,
|
|
432
|
+
...(taskOrigin.notify && !reportsBack ? { reportsBack: false } : {}),
|
|
433
|
+
};
|
|
190
434
|
}
|
|
191
435
|
|
|
192
|
-
/** The origin
|
|
193
|
-
|
|
436
|
+
/** The origin a `send` of agent `agentId` (from its home `callerSessionId`)
|
|
437
|
+
* reports to: the stored one of a task session it created, else one for
|
|
438
|
+
* this send only — never stored on the target, whose own origin and owner
|
|
439
|
+
* stay as they are. Null for a caller that is no agent conversation. */
|
|
440
|
+
async function reportOrigin(sessionId, agentId, callerSessionId) {
|
|
194
441
|
if (!agentId) return null;
|
|
195
|
-
const
|
|
196
|
-
|
|
442
|
+
const target = await loadSession(sessionId);
|
|
443
|
+
const origin = target?.taskOrigin;
|
|
444
|
+
if (origin?.agentId === agentId) return origin;
|
|
445
|
+
const title = (clean(target?.title) || sessionId).slice(0, TITLE_LIMIT);
|
|
446
|
+
// Files come back only from sessions the agent created (above) or its own
|
|
447
|
+
// room member threads; any other session reports its text alone.
|
|
448
|
+
const own = roomMemberOfSession(sessionId)?.agentId === agentId;
|
|
449
|
+
return {
|
|
450
|
+
agentId,
|
|
451
|
+
sessionId: callerSessionId,
|
|
452
|
+
title,
|
|
453
|
+
notify: true,
|
|
454
|
+
target: 'session',
|
|
455
|
+
...(own ? {} : { textOnly: true }),
|
|
456
|
+
};
|
|
197
457
|
}
|
|
198
458
|
|
|
199
|
-
return { create,
|
|
459
|
+
return { create, reportOrigin, recover, send: (sessionId, prompt, origin) => agentTurn(sessionId, prompt, origin) };
|
|
200
460
|
}
|
|
@@ -67,6 +67,7 @@ import { createConversationImportApi } from '../conversation-import/service.mjs'
|
|
|
67
67
|
import { importError } from '../conversation-import/discovery.mjs';
|
|
68
68
|
import { bootProfile } from '../boot-profile.mjs';
|
|
69
69
|
import { STANDALONE_DATA_DIR } from '../runtime-paths.mjs';
|
|
70
|
+
import { registerBotRouteStore } from '../../runtime/shared/agent-store.mjs';
|
|
70
71
|
import {
|
|
71
72
|
dataDirOf,
|
|
72
73
|
resolveRoute,
|
|
@@ -124,7 +125,16 @@ export function wireApis(boot) {
|
|
|
124
125
|
} catch (error) {
|
|
125
126
|
bootProfile('connector-policy:migrate-failed', { error: error?.message || String(error) });
|
|
126
127
|
}
|
|
128
|
+
// Bots get their own store (bots/) once, before any catalog read; a
|
|
129
|
+
// failed split leaves agents/ read as before (bot-split-migration.mjs).
|
|
130
|
+
try {
|
|
131
|
+
boot.workflowAgentsApi.migrateBotStore();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
bootProfile('bot-split:migrate-failed', { error: error?.message || String(error) });
|
|
134
|
+
}
|
|
127
135
|
}
|
|
136
|
+
// Route reads ask a store bot's own folder first (agent-store.mjs).
|
|
137
|
+
registerBotRouteStore(() => dataDirOf(boot.cfgMod));
|
|
128
138
|
boot.workspaceApi = workspaceApiFor(boot);
|
|
129
139
|
boot.sessionTurnApi = sessionTurnApiFor(boot);
|
|
130
140
|
const getFacadeSessionId = () => boot.rt.session?.id || boot.rt.reservedSessionId || null;
|
|
@@ -538,6 +548,8 @@ function modelRouteApiFor(boot) {
|
|
|
538
548
|
adoptConfig: boot.adoptConfig,
|
|
539
549
|
saveConfigAndAdopt: boot.saveConfigAndAdopt,
|
|
540
550
|
ensureFullConfig: boot.ensureFullConfig,
|
|
551
|
+
reloadFullConfig: boot.reloadFullConfig,
|
|
552
|
+
confirmLeadFill: boot.confirmLeadFill,
|
|
541
553
|
awaitKeychainPrewarm: boot.awaitKeychainPrewarm,
|
|
542
554
|
ensureProvidersReady: boot.ensureProvidersReady,
|
|
543
555
|
persistLeadRoute: boot.persistLeadRoute,
|
|
@@ -567,6 +579,7 @@ function workflowAgentsApiFor(boot) {
|
|
|
567
579
|
lookupModelMeta: boot.lookupModelMeta,
|
|
568
580
|
adoptConfig: boot.adoptConfig,
|
|
569
581
|
saveConfigAndAdopt: boot.saveConfigAndAdopt,
|
|
582
|
+
reloadFullConfig: boot.reloadFullConfig,
|
|
570
583
|
ensureProvidersReady: boot.ensureProvidersReady,
|
|
571
584
|
displayConfig: boot.displayConfig,
|
|
572
585
|
mcpStatus: () => boot.mcpStatus(),
|
|
@@ -11,7 +11,10 @@ import { LOCAL_PROVIDER_ID, configureLocalProviderIdleTtl } from '../../runtime/
|
|
|
11
11
|
import { resolve } from 'node:path';
|
|
12
12
|
import { clean } from '../../runtime/agent/orchestrator/runtime-core/session-text.mjs';
|
|
13
13
|
import { LAZY_SECRET_PROVIDERS } from '../../runtime/agent/orchestrator/runtime-core/model-capabilities.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
ensureProviderEnabled,
|
|
16
|
+
normalizeSystemShellConfig,
|
|
17
|
+
} from '../../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
|
|
15
18
|
import {
|
|
16
19
|
workflowPresetId,
|
|
17
20
|
normalizeWebSearchProviderId,
|
|
@@ -33,7 +36,7 @@ import { createProviderModels } from '../provider-models.mjs';
|
|
|
33
36
|
import { createProviderUsage } from '../provider-usage.mjs';
|
|
34
37
|
import { bootProfile } from '../boot-profile.mjs';
|
|
35
38
|
import { STANDALONE_DATA_DIR } from '../runtime-paths.mjs';
|
|
36
|
-
import { outputStyleStatus, webSearchCapableFor } from './shared.mjs';
|
|
39
|
+
import { outputStyleStatus, resolveRoute, webSearchCapableFor } from './shared.mjs';
|
|
37
40
|
|
|
38
41
|
export function wireProviders(boot) {
|
|
39
42
|
wireConfigLifecycle(boot);
|
|
@@ -63,6 +66,10 @@ function wireConfigLifecycle(boot) {
|
|
|
63
66
|
rt.configHasSecrets = next;
|
|
64
67
|
},
|
|
65
68
|
getRoute: () => rt.route,
|
|
69
|
+
setRouteState: (next) => {
|
|
70
|
+
rt.route = next;
|
|
71
|
+
},
|
|
72
|
+
resolveRoute,
|
|
66
73
|
cfgMod,
|
|
67
74
|
sharedCfgMod,
|
|
68
75
|
setConfiguredShell,
|
|
@@ -82,6 +89,7 @@ function wireConfigLifecycle(boot) {
|
|
|
82
89
|
seedOutputStyleStatusCache,
|
|
83
90
|
adoptConfig,
|
|
84
91
|
saveConfigAndAdopt,
|
|
92
|
+
confirmLeadFill,
|
|
85
93
|
scheduleSkillsSave,
|
|
86
94
|
flushSkillsSave,
|
|
87
95
|
scheduleOutputStyleSave,
|
|
@@ -98,6 +106,7 @@ function wireConfigLifecycle(boot) {
|
|
|
98
106
|
seedOutputStyleStatusCache,
|
|
99
107
|
adoptConfig,
|
|
100
108
|
saveConfigAndAdopt,
|
|
109
|
+
confirmLeadFill,
|
|
101
110
|
scheduleSkillsSave,
|
|
102
111
|
flushSkillsSave,
|
|
103
112
|
scheduleOutputStyleSave,
|
|
@@ -106,13 +115,14 @@ function wireConfigLifecycle(boot) {
|
|
|
106
115
|
ensureFullConfig,
|
|
107
116
|
displayConfig,
|
|
108
117
|
ensureConfigForRouteProvider,
|
|
109
|
-
persistLeadRoute: (routeLike) => {
|
|
118
|
+
persistLeadRoute: (routeLike, { onlyIfAbsent = false } = {}) => {
|
|
110
119
|
const leadRoute = normalizeWorkflowRoute(routeLike);
|
|
111
120
|
if (!leadRoute) return null;
|
|
112
121
|
const nextConfig = { ...(rt.config || {}) };
|
|
113
122
|
nextConfig.presets = upsertWorkflowPreset(nextConfig.presets, 'lead', leadRoute);
|
|
114
123
|
nextConfig.default = workflowPresetId('lead');
|
|
115
|
-
|
|
124
|
+
// A conditional fill reaches the file only while no Lead route is stored.
|
|
125
|
+
saveConfigAndAdopt(nextConfig, { leadFill: onlyIfAbsent });
|
|
116
126
|
return leadRoute;
|
|
117
127
|
},
|
|
118
128
|
});
|
|
@@ -79,6 +79,9 @@ export function createConfigLifecycle({
|
|
|
79
79
|
getConfigHasSecrets,
|
|
80
80
|
setConfigHasSecrets,
|
|
81
81
|
getRoute,
|
|
82
|
+
// Main route sync on reload (both optional: a lifecycle without them never syncs)
|
|
83
|
+
setRouteState,
|
|
84
|
+
resolveRoute,
|
|
82
85
|
// shared modules / helpers
|
|
83
86
|
cfgMod,
|
|
84
87
|
sharedCfgMod,
|
|
@@ -121,16 +124,42 @@ export function createConfigLifecycle({
|
|
|
121
124
|
// separate from adoptConfig: model tuning is adopted before its route save.
|
|
122
125
|
let configSaveBaseline = structuredClone(getConfig());
|
|
123
126
|
|
|
124
|
-
|
|
127
|
+
// The Lead route lives in these two stored keys; a conditional fill writes
|
|
128
|
+
// them only while `default` is still unset on disk, judged under the config
|
|
129
|
+
// file lock when the patch is applied (config-patch.mjs `unlessSet`).
|
|
130
|
+
const LEAD_FILL_KEYS = new Set(['default', 'presets']);
|
|
131
|
+
|
|
132
|
+
function saveConfigAndAdopt(nextConfig, { hasSecrets = getConfigHasSecrets(), leadFill = false } = {}) {
|
|
125
133
|
// In-memory adopt is synchronous and first so callers that read back the
|
|
126
134
|
// value immediately (e.g. setProfile -> getProfile) see the new state.
|
|
127
135
|
const adopted = adoptConfig(nextConfig, { hasSecrets });
|
|
128
|
-
const changes = cfgMod
|
|
136
|
+
const changes = cfgMod
|
|
137
|
+
.createConfigPatch(configSaveBaseline, adopted)
|
|
138
|
+
.map((change) =>
|
|
139
|
+
leadFill && LEAD_FILL_KEYS.has(change.path[0]) ? { ...change, unlessSet: ['default'] } : change
|
|
140
|
+
);
|
|
129
141
|
configSaveBaseline = structuredClone(adopted);
|
|
130
142
|
if (changes.length) writers.queueConfigChanges([...changes, ...seedChanges(adopted, changes)]);
|
|
131
143
|
return adopted;
|
|
132
144
|
}
|
|
133
145
|
|
|
146
|
+
const sameModel = (a, b) => clean(a?.provider) === clean(b?.provider) && clean(a?.model) === clean(b?.model);
|
|
147
|
+
|
|
148
|
+
/** After a conditional Lead fill adopted `filled`: persist it now and, when
|
|
149
|
+
* the file already held another Lead route (written by another process after
|
|
150
|
+
* the fill's re-read, so the patch skipped it), adopt that one instead.
|
|
151
|
+
* Answers whether the runtime route was replaced. A route the user has moved
|
|
152
|
+
* off `filled` meanwhile stays. */
|
|
153
|
+
async function confirmLeadFill(filled) {
|
|
154
|
+
await writers.flushConfigSaveAsync();
|
|
155
|
+
reloadFullConfig();
|
|
156
|
+
if (!setRouteState || !resolveRoute || !sameModel(getRoute(), filled)) return false;
|
|
157
|
+
const winner = resolveRoute(getConfig(), {});
|
|
158
|
+
if (!clean(winner?.model) || sameModel(winner, filled)) return false;
|
|
159
|
+
setRouteState(winner);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
134
163
|
// A fresh profile's install markers (an empty `builtins`) equal the
|
|
135
164
|
// defaults, so no diff ever carries them — yet on disk their absence reads
|
|
136
165
|
// as a grandfathered profile. The first write seeds them where the stored
|
|
@@ -177,9 +206,20 @@ export function createConfigLifecycle({
|
|
|
177
206
|
}
|
|
178
207
|
const adopted = adoptConfig(next, { hasSecrets: true });
|
|
179
208
|
configSaveBaseline = structuredClone(adopted);
|
|
209
|
+
syncEmptyMainRoute(adopted);
|
|
180
210
|
return adopted;
|
|
181
211
|
}
|
|
182
212
|
|
|
213
|
+
// A Main route the runtime still has no model for takes the Lead route a
|
|
214
|
+
// reload brings in (edited on disk or by another process), so a conditional
|
|
215
|
+
// backfill sees it and never overwrites it. A live route that has a model is
|
|
216
|
+
// the user's selection and stays.
|
|
217
|
+
function syncEmptyMainRoute(config) {
|
|
218
|
+
if (!setRouteState || !resolveRoute || clean(getRoute()?.model)) return;
|
|
219
|
+
const resolved = resolveRoute(config, {});
|
|
220
|
+
if (clean(resolved?.model)) setRouteState(resolved);
|
|
221
|
+
}
|
|
222
|
+
|
|
183
223
|
function ensureFullConfig() {
|
|
184
224
|
if (getConfigHasSecrets()) return getConfig();
|
|
185
225
|
return reloadFullConfig();
|
|
@@ -211,6 +251,7 @@ export function createConfigLifecycle({
|
|
|
211
251
|
// adopt / save
|
|
212
252
|
adoptConfig,
|
|
213
253
|
saveConfigAndAdopt,
|
|
254
|
+
confirmLeadFill,
|
|
214
255
|
// Skills publication also drains older config edits first.
|
|
215
256
|
flushSkillsSave: writers.flushConfigSaveAsync,
|
|
216
257
|
scheduleSkillsSave,
|