mixdog 0.9.97 → 0.9.99
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/package.json +9 -2
- package/src/rules/agent/30-explorer.md +43 -32
- package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -84
- package/src/runtime/attachments/pdf-extract.mjs +83 -0
- package/src/runtime/attachments/store.mjs +515 -0
- package/src/runtime/attachments/store.test.mjs +188 -0
- package/src/runtime/channels/lib/interaction-handlers.mjs +1 -191
- package/src/runtime/channels/lib/runtime-paths.mjs +1 -4
- package/src/runtime/channels/lib/worker-ipc.mjs +0 -66
- package/src/runtime/channels/lib/worker-main.mjs +1 -20
- package/src/runtime/memory/lib/http-wire.mjs +39 -4
- package/src/runtime/shared/json-metrics.mjs +94 -0
- package/src/runtime/shared/turn-snapshot.mjs +13 -0
- package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
- package/src/session-runtime/provider-models.mjs +11 -1
- package/src/session-runtime/quick-model-rows.mjs +38 -7
- package/src/session-runtime/quick-search-models.mjs +15 -17
- package/src/session-runtime/runtime-core.mjs +9 -4
- package/src/session-runtime/runtime-tunables.mjs +9 -6
- package/src/session-runtime/session-title.mjs +27 -3
- package/src/standalone/channel-transport.mjs +28 -12
- package/src/standalone/daemon.mjs +3 -0
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/standalone/session-service.mjs +23 -1
- package/src/standalone/session-transport.mjs +3 -2
- package/src/tui/app/prompt-submit.mjs +7 -9
- package/src/tui/app/use-prompt-handlers.mjs +7 -3
- package/src/tui/dist/index.mjs +249 -43
- package/src/tui/paste-attachments.mjs +13 -7
- package/src/tui/prompt-history-store.mjs +52 -4
- package/src/tui/session/agent-job-feed.mjs +5 -1
- package/src/tui/session/live-share.mjs +21 -7
- package/src/tui/session/queue-helpers.mjs +6 -6
- package/src/tui/session/session-api-ext.mjs +12 -4
- package/src/tui/session/session-api.mjs +10 -1
- package/src/tui/session/session-flow.mjs +4 -2
- package/src/tui/session-local.mjs +15 -5
|
@@ -5,6 +5,7 @@ import { clean } from './session-text.mjs';
|
|
|
5
5
|
import { QUICK_SEARCH_MODELS } from './quick-search-models.mjs';
|
|
6
6
|
import { SEARCH_DEFAULT_PROVIDER, SEARCH_DEFAULT_MODEL } from './workflow.mjs';
|
|
7
7
|
import { getModelMetadataSync } from '../runtime/agent/orchestrator/providers/model-catalog.mjs';
|
|
8
|
+
import { providerCachedModelsSync } from '../runtime/agent/orchestrator/providers/provider-catalog-cache.mjs';
|
|
8
9
|
|
|
9
10
|
function metadataFor(provider, modelId) {
|
|
10
11
|
try {
|
|
@@ -51,15 +52,39 @@ export function createQuickModelRows({
|
|
|
51
52
|
const pickerConfig = displayConfig();
|
|
52
53
|
const rows = [];
|
|
53
54
|
const seen = new Set();
|
|
55
|
+
const addModel = (provider, modelLike = {}) => {
|
|
56
|
+
const model = modelLike && typeof modelLike === 'object'
|
|
57
|
+
? modelLike
|
|
58
|
+
: { id: clean(modelLike) };
|
|
59
|
+
const modelId = clean(model.id || model.name);
|
|
60
|
+
if (!provider || !modelId) return;
|
|
61
|
+
const key = `${provider}:${modelId}`;
|
|
62
|
+
if (seen.has(key)) return;
|
|
63
|
+
seen.add(key);
|
|
64
|
+
const meta = metadataFor(provider, modelId);
|
|
65
|
+
const row = providerModelCacheRow(provider, {
|
|
66
|
+
...model,
|
|
67
|
+
id: modelId,
|
|
68
|
+
name: model.name || model.display || modelId,
|
|
69
|
+
display: model.display || model.name || modelId,
|
|
70
|
+
contextWindow: model.contextWindow || meta.contextWindow || null,
|
|
71
|
+
outputTokens: model.outputTokens || meta.outputTokens || null,
|
|
72
|
+
supportsReasoning: model.supportsReasoning === true || meta.supportsReasoning === true,
|
|
73
|
+
supportsFunctionCalling: model.supportsFunctionCalling === true || meta.supportsFunctionCalling === true,
|
|
74
|
+
supportsPromptCaching: model.supportsPromptCaching === true || meta.supportsPromptCaching === true,
|
|
75
|
+
reasoningOptions: model.reasoningOptions?.length ? model.reasoningOptions : (meta.reasoningOptions || []),
|
|
76
|
+
reasoningContentField: model.reasoningContentField || meta.reasoningContentField || null,
|
|
77
|
+
mode: model.mode || meta.mode || 'chat',
|
|
78
|
+
});
|
|
79
|
+
rows.push(row);
|
|
80
|
+
modelMetaByRoute.set(modelMetaKey(provider, modelId), row);
|
|
81
|
+
};
|
|
54
82
|
const addRoute = (routeLike = {}) => {
|
|
55
83
|
const provider = clean(routeLike.provider);
|
|
56
84
|
const model = clean(routeLike.model);
|
|
57
85
|
if (!provider || !model) return;
|
|
58
|
-
const key = `${provider}:${model}`;
|
|
59
|
-
if (seen.has(key)) return;
|
|
60
|
-
seen.add(key);
|
|
61
86
|
const meta = metadataFor(provider, model);
|
|
62
|
-
|
|
87
|
+
addModel(provider, {
|
|
63
88
|
id: model,
|
|
64
89
|
name: routeLike.modelDisplay || routeLike.display || model,
|
|
65
90
|
display: routeLike.modelDisplay || routeLike.display || model,
|
|
@@ -73,10 +98,12 @@ export function createQuickModelRows({
|
|
|
73
98
|
reasoningContentField: meta.reasoningContentField || null,
|
|
74
99
|
mode: 'chat',
|
|
75
100
|
});
|
|
76
|
-
rows.push(row);
|
|
77
|
-
modelMetaByRoute.set(modelMetaKey(provider, model), row);
|
|
78
101
|
};
|
|
79
102
|
|
|
103
|
+
for (const [provider, providerConfig] of Object.entries(pickerConfig.providers || {})) {
|
|
104
|
+
if (!providerConfig?.enabled) continue;
|
|
105
|
+
for (const model of providerCachedModelsSync(provider)) addModel(provider, model);
|
|
106
|
+
}
|
|
80
107
|
addRoute(route);
|
|
81
108
|
for (const preset of pickerConfig.presets || []) addRoute(preset);
|
|
82
109
|
for (const workflowRoute of Object.values(pickerConfig.workflowRoutes || {})) addRoute(workflowRoute);
|
|
@@ -146,7 +173,11 @@ export function createQuickModelRows({
|
|
|
146
173
|
for (const [name, providerConfig] of Object.entries(pickerConfig.providers || {})) {
|
|
147
174
|
const providerName = normalizeSearchProviderId(name);
|
|
148
175
|
if (!providerConfig?.enabled || !isSearchCapableProvider(providerName)) continue;
|
|
149
|
-
|
|
176
|
+
const cachedModels = providerCachedModelsSync(providerName);
|
|
177
|
+
const quickModels = cachedModels.length
|
|
178
|
+
? cachedModels
|
|
179
|
+
: (QUICK_SEARCH_MODELS[providerName] || []);
|
|
180
|
+
for (const model of quickModels) {
|
|
150
181
|
addQuickSearchModel(rows, seen, providerName, model);
|
|
151
182
|
}
|
|
152
183
|
}
|
|
@@ -6,10 +6,11 @@ export const ONBOARDING_VERSION = 1;
|
|
|
6
6
|
|
|
7
7
|
export const QUICK_SEARCH_MODELS = Object.freeze({
|
|
8
8
|
'openai-oauth': [
|
|
9
|
+
{ id: 'gpt-5.6-sol', display: 'GPT-5.6 Sol', latest: true },
|
|
10
|
+
{ id: 'gpt-5.6-terra', display: 'GPT-5.6 Terra', latest: true },
|
|
11
|
+
{ id: 'gpt-5.6-luna', display: 'GPT-5.6 Luna' },
|
|
9
12
|
{ id: 'gpt-5.5', display: 'GPT-5.5', latest: true },
|
|
10
|
-
{ id: 'gpt-5.4', display: 'GPT-5.4'
|
|
11
|
-
{ id: 'gpt-5', display: 'GPT-5' },
|
|
12
|
-
{ id: 'gpt-4.1', display: 'GPT-4.1' },
|
|
13
|
+
{ id: 'gpt-5.4-mini', display: 'GPT-5.4 Mini' },
|
|
13
14
|
],
|
|
14
15
|
openai: [
|
|
15
16
|
{ id: 'gpt-5.5', display: 'GPT-5.5', latest: true },
|
|
@@ -19,29 +20,26 @@ export const QUICK_SEARCH_MODELS = Object.freeze({
|
|
|
19
20
|
{ id: 'gpt-4o', display: 'GPT-4o' },
|
|
20
21
|
],
|
|
21
22
|
'grok-oauth': [
|
|
22
|
-
{ id: 'grok-4.
|
|
23
|
-
{ id: 'grok-4.20', display: 'Grok 4.20' },
|
|
24
|
-
{ id: 'grok-4', display: 'Grok 4' },
|
|
23
|
+
{ id: 'grok-4.5', display: 'Grok 4.5', latest: true },
|
|
25
24
|
],
|
|
26
25
|
xai: [
|
|
27
|
-
{ id: 'grok-4.
|
|
28
|
-
{ id: 'grok-4.20', display: 'Grok 4.20' },
|
|
29
|
-
{ id: 'grok-4', display: 'Grok 4' },
|
|
26
|
+
{ id: 'grok-4.5', display: 'Grok 4.5', latest: true },
|
|
30
27
|
],
|
|
31
28
|
gemini: [
|
|
32
|
-
{ id: 'gemini-3-
|
|
33
|
-
{ id: 'gemini-
|
|
34
|
-
{ id: 'gemini-
|
|
35
|
-
{ id: 'gemini-2.0-flash', display: 'Gemini 2.0 Flash' },
|
|
29
|
+
{ id: 'gemini-3.6-flash', display: 'Gemini 3.6 Flash', latest: true },
|
|
30
|
+
{ id: 'gemini-3.5-flash-lite', display: 'Gemini 3.5 Flash Lite', latest: true },
|
|
31
|
+
{ id: 'gemini-3.1-pro-preview', display: 'Gemini 3.1 Pro Preview' },
|
|
36
32
|
],
|
|
37
33
|
'anthropic-oauth': [
|
|
38
|
-
{ id: 'claude-opus-
|
|
39
|
-
{ id: 'claude-sonnet-
|
|
34
|
+
{ id: 'claude-opus-5', display: 'Claude Opus 5', latest: true },
|
|
35
|
+
{ id: 'claude-sonnet-5', display: 'Claude Sonnet 5', latest: true },
|
|
36
|
+
{ id: 'claude-fable-5', display: 'Claude Fable 5', latest: true },
|
|
40
37
|
{ id: 'claude-haiku-4-5-20251001', display: 'Claude Haiku 4.5' },
|
|
41
38
|
],
|
|
42
39
|
anthropic: [
|
|
43
|
-
{ id: 'claude-opus-
|
|
44
|
-
{ id: 'claude-sonnet-
|
|
40
|
+
{ id: 'claude-opus-5', display: 'Claude Opus 5', latest: true },
|
|
41
|
+
{ id: 'claude-sonnet-5', display: 'Claude Sonnet 5', latest: true },
|
|
42
|
+
{ id: 'claude-fable-5', display: 'Claude Fable 5', latest: true },
|
|
45
43
|
{ id: 'claude-haiku-4-5-20251001', display: 'Claude Haiku 4.5' },
|
|
46
44
|
],
|
|
47
45
|
});
|
|
@@ -229,6 +229,7 @@ import { createWarmupSchedulers } from './warmup-schedulers.mjs';
|
|
|
229
229
|
import { createPrewarmSchedulers } from './prewarm.mjs';
|
|
230
230
|
import {
|
|
231
231
|
getTurnReviewDiff as getTurnSnapshotReviewDiff,
|
|
232
|
+
revertTurnReview as revertTurnSnapshotReview,
|
|
232
233
|
revertTurnReviewFile as revertTurnSnapshotReviewFile,
|
|
233
234
|
} from '../runtime/shared/turn-snapshot.mjs';
|
|
234
235
|
import { createMcpGlue } from './mcp-glue.mjs';
|
|
@@ -1303,10 +1304,10 @@ export async function createMixdogSessionRuntime({
|
|
|
1303
1304
|
scheduleProviderSetupWarmup();
|
|
1304
1305
|
scheduleModelCatalogWarmup();
|
|
1305
1306
|
scheduleProviderWarmup();
|
|
1306
|
-
//
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1307
|
+
// Daemon-hosted product sessions warm the provider model catalog immediately
|
|
1308
|
+
// in the background. This does not delay runtime readiness; an early picker
|
|
1309
|
+
// joins the boot prefetch instead of starting duplicate provider I/O.
|
|
1310
|
+
// Standalone runtimes retain an exit-safe grace period (runtime-tunables).
|
|
1310
1311
|
scheduleProviderModelWarmup();
|
|
1311
1312
|
scheduleStatuslineUsageWarmup();
|
|
1312
1313
|
// Channels are opt-in: only boot the worker when this session started in (or
|
|
@@ -1649,6 +1650,10 @@ export async function createMixdogSessionRuntime({
|
|
|
1649
1650
|
rt.session?.id,
|
|
1650
1651
|
options,
|
|
1651
1652
|
),
|
|
1653
|
+
revertTurnReview: () => revertTurnSnapshotReview(
|
|
1654
|
+
rt.currentCwd,
|
|
1655
|
+
rt.session?.id,
|
|
1656
|
+
),
|
|
1652
1657
|
revertTurnReviewFile: (file) => revertTurnSnapshotReviewFile(
|
|
1653
1658
|
rt.currentCwd,
|
|
1654
1659
|
rt.session?.id,
|
|
@@ -6,16 +6,19 @@ import { envDelayMs, envFlag, envPresent } from './env.mjs';
|
|
|
6
6
|
|
|
7
7
|
export function readRuntimeTunables() {
|
|
8
8
|
const codeGraphPrewarmEnabled = !envFlag('MIXDOG_DISABLE_CODE_GRAPH_PREWARM');
|
|
9
|
+
const daemonHosted = envFlag('MIXDOG_DAEMON_HOST');
|
|
9
10
|
return {
|
|
10
11
|
providerSetupWarmupDelayMs: envDelayMs('MIXDOG_PROVIDER_SETUP_WARMUP_DELAY_MS', 300, { min: 0, max: 60_000 }),
|
|
11
12
|
modelCatalogWarmupDelayMs: envDelayMs('MIXDOG_MODEL_CATALOG_WARMUP_DELAY_MS', 200, { min: 0, max: 60_000 }),
|
|
12
13
|
providerWarmupDelayMs: envDelayMs('MIXDOG_PROVIDER_WARMUP_DELAY_MS', 1_500, { min: 0, max: 60_000 }),
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
// A product session lives in the long-running daemon, so start its model
|
|
15
|
+
// prefetch on the first background tick. Standalone/short-lived runtimes
|
|
16
|
+
// keep the old grace period so provider sockets cannot hold process exit.
|
|
17
|
+
providerModelWarmupDelayMs: envDelayMs(
|
|
18
|
+
'MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS',
|
|
19
|
+
daemonHosted ? 0 : 2_000,
|
|
20
|
+
{ min: 0, max: 120_000 },
|
|
21
|
+
),
|
|
19
22
|
codeGraphPrewarmDelayMs: envDelayMs('MIXDOG_CODE_GRAPH_PREWARM_DELAY_MS', 250, { min: 0, max: 60_000 }),
|
|
20
23
|
statuslineUsageWarmupDelayMs: envDelayMs('MIXDOG_STATUSLINE_USAGE_WARMUP_DELAY_MS', 800, { min: 0, max: 60_000 }),
|
|
21
24
|
// Idle keep-alive: re-fetch usage before the statusline's 10-min staleness
|
|
@@ -111,11 +111,24 @@ export function createSessionTitleController(deps = {}) {
|
|
|
111
111
|
const firstAttempts = new Set();
|
|
112
112
|
const thirdAttempts = new Set();
|
|
113
113
|
const active = new Map();
|
|
114
|
+
const attemptLimit = Number.isFinite(Number(deps.attemptLimit))
|
|
115
|
+
? Math.max(1, Number(deps.attemptLimit))
|
|
116
|
+
: 2048;
|
|
114
117
|
const timeoutMs = Number.isFinite(Number(deps.timeoutMs))
|
|
115
118
|
? Math.max(1, Number(deps.timeoutMs))
|
|
116
119
|
: SESSION_TITLE_TIMEOUT_MS;
|
|
117
120
|
let disposed = false;
|
|
118
121
|
|
|
122
|
+
const rememberAttempt = (attempts, sessionId) => {
|
|
123
|
+
attempts.delete(sessionId);
|
|
124
|
+
attempts.add(sessionId);
|
|
125
|
+
while (attempts.size > attemptLimit) {
|
|
126
|
+
const oldest = attempts.values().next().value;
|
|
127
|
+
if (oldest === undefined) break;
|
|
128
|
+
attempts.delete(oldest);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
119
132
|
const log = (line) => {
|
|
120
133
|
const message = `[mixdog] llm-title ${line}`;
|
|
121
134
|
try { deps.log?.(message); } catch {}
|
|
@@ -190,7 +203,7 @@ export function createSessionTitleController(deps = {}) {
|
|
|
190
203
|
if (priorMeaningfulUser) return false;
|
|
191
204
|
const source = firstTurnTitleSource(prompt);
|
|
192
205
|
if (!source) return false;
|
|
193
|
-
firstAttempts
|
|
206
|
+
rememberAttempt(firstAttempts, sessionId);
|
|
194
207
|
const greeting = greetingTitle(source);
|
|
195
208
|
if (greeting) {
|
|
196
209
|
log(`generated id=${sessionId} stage=first title=${JSON.stringify(greeting)} deterministic=greeting`);
|
|
@@ -211,7 +224,7 @@ export function createSessionTitleController(deps = {}) {
|
|
|
211
224
|
|| session?.generatedTitleStage === 'third') return false;
|
|
212
225
|
const source = thirdTurnTitleSource(session?.messages);
|
|
213
226
|
if (!source) return false;
|
|
214
|
-
thirdAttempts
|
|
227
|
+
rememberAttempt(thirdAttempts, sessionId);
|
|
215
228
|
run(sessionId, source, 'third', thirdAttempts);
|
|
216
229
|
return true;
|
|
217
230
|
}
|
|
@@ -222,7 +235,18 @@ export function createSessionTitleController(deps = {}) {
|
|
|
222
235
|
abort.abort(new Error('Session title generation disposed.'));
|
|
223
236
|
}
|
|
224
237
|
active.clear();
|
|
238
|
+
firstAttempts.clear();
|
|
239
|
+
thirdAttempts.clear();
|
|
225
240
|
}
|
|
226
241
|
|
|
227
|
-
return {
|
|
242
|
+
return {
|
|
243
|
+
scheduleFirst,
|
|
244
|
+
observeThird,
|
|
245
|
+
disposeAll,
|
|
246
|
+
attemptStatsForTest: () => ({
|
|
247
|
+
first: firstAttempts.size,
|
|
248
|
+
third: thirdAttempts.size,
|
|
249
|
+
limit: attemptLimit,
|
|
250
|
+
}),
|
|
251
|
+
};
|
|
228
252
|
}
|
|
@@ -20,6 +20,12 @@ import { writeJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
|
|
|
20
20
|
import { readBody, sendJson, sendError } from '../runtime/memory/lib/http-wire.mjs';
|
|
21
21
|
import { createFairCallScheduler } from './fair-call-scheduler.mjs';
|
|
22
22
|
|
|
23
|
+
export const CHANNEL_HTTP_BODY_MAX_BYTES = 64 * 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
function readChannelBody(req) {
|
|
26
|
+
return readBody(req, { maxBytes: CHANNEL_HTTP_BODY_MAX_BYTES });
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
function parsePid(value) {
|
|
24
30
|
const n = Number(value);
|
|
25
31
|
return Number.isInteger(n) && n > 0 ? n : null;
|
|
@@ -425,7 +431,13 @@ export function createChannelTransport({
|
|
|
425
431
|
function writeRemoteStateTo(client, state) {
|
|
426
432
|
if (!client) return false;
|
|
427
433
|
const frame = JSON.stringify({ type: 'notify', method: REMOTE_STATE_METHOD, params: { state } });
|
|
428
|
-
if (!client.sse) {
|
|
434
|
+
if (!client.sse) {
|
|
435
|
+
// This is state, not an event log. Ownership churn before an SSE
|
|
436
|
+
// reconnect only needs the newest transition; retaining every displaced
|
|
437
|
+
// frame lets one disconnected client grow without bound.
|
|
438
|
+
client.pendingRemoteStateFrame = frame;
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
429
441
|
try { client.sse.write(`data: ${frame}\n\n`); return true; }
|
|
430
442
|
catch (err) { log(`remote-state '${state}' write failed for lead=${client.leadPid}: ${err?.message || err}`); return false; }
|
|
431
443
|
}
|
|
@@ -539,7 +551,7 @@ export function createChannelTransport({
|
|
|
539
551
|
leadPid: pid,
|
|
540
552
|
cwd: cwd || null,
|
|
541
553
|
sse: null,
|
|
542
|
-
|
|
554
|
+
pendingRemoteStateFrame: null,
|
|
543
555
|
lastSeen: nowMs(),
|
|
544
556
|
registeredAt: nowMs(),
|
|
545
557
|
});
|
|
@@ -574,7 +586,10 @@ export function createChannelTransport({
|
|
|
574
586
|
const replacedWasPointer = pointerToken === replaced.token;
|
|
575
587
|
// Token-scoped state belongs to the logical client, not only the owner.
|
|
576
588
|
// A non-owner can hold a buffered superseded frame or stored bind intent.
|
|
577
|
-
if (replaced.
|
|
589
|
+
if (replaced.pendingRemoteStateFrame) {
|
|
590
|
+
fresh.pendingRemoteStateFrame = replaced.pendingRemoteStateFrame;
|
|
591
|
+
replaced.pendingRemoteStateFrame = null;
|
|
592
|
+
}
|
|
578
593
|
if (replaced.remoteSessionId) fresh.remoteSessionId = replaced.remoteSessionId;
|
|
579
594
|
if (replacedWasPointer) pointerToken = token;
|
|
580
595
|
removeClientRecord(replaced.token);
|
|
@@ -605,10 +620,10 @@ export function createChannelTransport({
|
|
|
605
620
|
c.lastSeen = nowMs();
|
|
606
621
|
// Flush any targeted frames buffered while this client had no stream (e.g. a
|
|
607
622
|
// 'superseded' emitted at the moment it was reconnecting). Drop-with-client.
|
|
608
|
-
if (c.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
623
|
+
if (c.pendingRemoteStateFrame) {
|
|
624
|
+
const frame = c.pendingRemoteStateFrame;
|
|
625
|
+
c.pendingRemoteStateFrame = null;
|
|
626
|
+
try { res.write(`data: ${frame}\n\n`); } catch {}
|
|
612
627
|
}
|
|
613
628
|
// Replay the sticky 'acquired' badge ONLY when THIS attaching client is the
|
|
614
629
|
// current pointer (the owner). A non-pointer late attach must NOT light the
|
|
@@ -661,7 +676,7 @@ export function createChannelTransport({
|
|
|
661
676
|
sendError(res, `daemon is draining: ${drainingReason}`, 503);
|
|
662
677
|
return;
|
|
663
678
|
}
|
|
664
|
-
const body = await
|
|
679
|
+
const body = await readChannelBody(req);
|
|
665
680
|
const clientToken = registerClient({
|
|
666
681
|
leadPid: body.leadPid, cwd: body.cwd, passive: body.passive === true,
|
|
667
682
|
replaceToken: body.replaceToken, registrationId: body.registrationId,
|
|
@@ -672,7 +687,7 @@ export function createChannelTransport({
|
|
|
672
687
|
return;
|
|
673
688
|
}
|
|
674
689
|
if (req.method === 'POST' && pathName === '/client/deregister') {
|
|
675
|
-
const body = await
|
|
690
|
+
const body = await readChannelBody(req);
|
|
676
691
|
if (body.registrationId) {
|
|
677
692
|
const cancelled = cancelReplacementRegistration(body);
|
|
678
693
|
if (cancelled === 'forbidden') { sendError(res, 'forbidden replacement deregister', 403); return; }
|
|
@@ -695,7 +710,7 @@ export function createChannelTransport({
|
|
|
695
710
|
// parallel fair scheduler and per-call cancellation.
|
|
696
711
|
if (req.method === 'POST' && pathName === '/agent/dispatch') {
|
|
697
712
|
if (!agentBroker?.dispatch) { sendError(res, 'agent broker unavailable', 503); return; }
|
|
698
|
-
const body = await
|
|
713
|
+
const body = await readChannelBody(req);
|
|
699
714
|
const callId = String(body.callId || '').trim();
|
|
700
715
|
if (!callId) { sendError(res, 'callId required', 400); return; }
|
|
701
716
|
res.on('close', () => {
|
|
@@ -712,7 +727,7 @@ export function createChannelTransport({
|
|
|
712
727
|
}
|
|
713
728
|
if (req.method === 'POST' && pathName === '/agent/cancel') {
|
|
714
729
|
if (!agentBroker?.cancel) { sendError(res, 'agent broker unavailable', 503); return; }
|
|
715
|
-
const body = await
|
|
730
|
+
const body = await readChannelBody(req);
|
|
716
731
|
const callId = String(body.callId || '').trim();
|
|
717
732
|
if (!callId) { sendError(res, 'callId required', 400); return; }
|
|
718
733
|
const cancelled = agentBroker.cancelAndWait
|
|
@@ -728,7 +743,7 @@ export function createChannelTransport({
|
|
|
728
743
|
return;
|
|
729
744
|
}
|
|
730
745
|
if (req.method === 'POST' && pathName === '/call') {
|
|
731
|
-
const body = await
|
|
746
|
+
const body = await readChannelBody(req);
|
|
732
747
|
const clientToken = body.token || null;
|
|
733
748
|
const c = clientToken ? clients.get(clientToken) : null;
|
|
734
749
|
if (!c) { sendError(res, 'unknown client token', 404); return; }
|
|
@@ -920,6 +935,7 @@ export function createChannelTransport({
|
|
|
920
935
|
_clientsForTest: clients,
|
|
921
936
|
_registrationReplaysForTest: registrationReplays,
|
|
922
937
|
_resolveTargetForTest: resolveTarget,
|
|
938
|
+
_writeRemoteStateToForTest: writeRemoteStateTo,
|
|
923
939
|
get _pointerTokenForTest() { return pointerToken; },
|
|
924
940
|
get _remoteIntentForTest() { return remoteIntent; },
|
|
925
941
|
};
|
|
@@ -526,6 +526,9 @@ async function main() {
|
|
|
526
526
|
function prewarmSessionRuntime() {
|
|
527
527
|
if (sessionRuntimePrewarmStarted) return;
|
|
528
528
|
sessionRuntimePrewarmStarted = true;
|
|
529
|
+
void import('../runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs')
|
|
530
|
+
.then((module) => module.prewarmImageResizer?.())
|
|
531
|
+
.catch((error) => log(`image pipeline prewarm failed (non-fatal): ${error?.message || error}`));
|
|
529
532
|
void sessionRuntimePool.prewarm()
|
|
530
533
|
.then(() => log('session shard runtime/agent-loop/keychain prewarm started'))
|
|
531
534
|
.catch((error) => {
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
SESSION_CONFIGURE_ACTION_SET,
|
|
16
16
|
SESSION_READ_ACTION_SET,
|
|
17
17
|
} from './session-protocol.mjs';
|
|
18
|
+
import {
|
|
19
|
+
materializePromptSubmission,
|
|
20
|
+
preparePromptSubmissionForProvider,
|
|
21
|
+
} from '../runtime/attachments/store.mjs';
|
|
18
22
|
|
|
19
23
|
const MAX_CLONE_DEPTH = 24;
|
|
20
24
|
const requireDesktopService = createRequire(import.meta.url);
|
|
@@ -272,6 +276,20 @@ export function createSessionService({
|
|
|
272
276
|
const items = entry.itemCache ??= new Map();
|
|
273
277
|
const out = {};
|
|
274
278
|
for (const [key, value] of Object.entries(raw)) {
|
|
279
|
+
if (key === 'queued' && Array.isArray(value)) {
|
|
280
|
+
const projected = value.map((item) => ({
|
|
281
|
+
id: item?.id,
|
|
282
|
+
submittedAt: item?.submittedAt,
|
|
283
|
+
text: String(item?.displayText ?? item?.text ?? '').slice(0, 2_000),
|
|
284
|
+
displayText: String(item?.displayText ?? item?.text ?? '').slice(0, 2_000),
|
|
285
|
+
mode: item?.mode || 'prompt',
|
|
286
|
+
priority: item?.priority || 'next',
|
|
287
|
+
...(Array.isArray(item?.images) && item.images.length ? { images: item.images } : {}),
|
|
288
|
+
}));
|
|
289
|
+
fields.set(key, { source: value, value: projected });
|
|
290
|
+
out[key] = projected;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
275
293
|
if (key === 'items' && Array.isArray(value)) {
|
|
276
294
|
const nextItems = new Map();
|
|
277
295
|
out.items = value.map((item) => {
|
|
@@ -815,10 +833,14 @@ export function createSessionService({
|
|
|
815
833
|
const entry = await entryForSession(id, openHints || {});
|
|
816
834
|
const target = entry.runtime.submitAsync;
|
|
817
835
|
if (typeof target !== 'function') throw new TypeError('session runtime must implement submitAsync');
|
|
836
|
+
const intake = await preparePromptSubmissionForProvider(
|
|
837
|
+
materializePromptSubmission(prompt, options || {}),
|
|
838
|
+
entry.runtime.provider || entry.runtime.session?.provider || '',
|
|
839
|
+
);
|
|
818
840
|
// Await intake only: submitAsync resolves once the prompt is represented by
|
|
819
841
|
// the queue/user row, while provider execution remains daemon-owned and
|
|
820
842
|
// detached.
|
|
821
|
-
const accepted = await Promise.resolve(target.call(entry.runtime, prompt, options
|
|
843
|
+
const accepted = await Promise.resolve(target.call(entry.runtime, intake.prompt, intake.options));
|
|
822
844
|
const firstSubmit = accepted === true && entry.reservedOnly;
|
|
823
845
|
if (accepted === true) {
|
|
824
846
|
entry.reservedOnly = false;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// the same transport is exercised by the real daemon entry AND by the smoke
|
|
13
13
|
// harness with a stub session runtime (no provider, no model download).
|
|
14
14
|
import http from 'node:http';
|
|
15
|
-
import {
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
16
16
|
import { sendJson, sendError } from '../runtime/memory/lib/http-wire.mjs';
|
|
17
17
|
import {
|
|
18
18
|
compareRuntimeVersions,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
runtimeVersion,
|
|
23
23
|
} from './session-wire.mjs';
|
|
24
24
|
import { createFairCallScheduler } from './fair-call-scheduler.mjs';
|
|
25
|
+
import { hashStructuredValue } from '../runtime/shared/json-metrics.mjs';
|
|
25
26
|
|
|
26
27
|
// A loopback front door still buffers whatever a client sends before it can be
|
|
27
28
|
// parsed, so the body has an explicit ceiling instead of the client's memory.
|
|
@@ -93,7 +94,7 @@ function readLimitedBody(req, {
|
|
|
93
94
|
* submission id that happens to be reused carries a different one. */
|
|
94
95
|
function callSignature(name, args) {
|
|
95
96
|
try {
|
|
96
|
-
return
|
|
97
|
+
return hashStructuredValue({ name, args: args ?? {} });
|
|
97
98
|
} catch { return null; }
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
import { projectNameFromPath } from './app-format.mjs';
|
|
13
13
|
import {
|
|
14
14
|
buildPromptContentWithImages,
|
|
15
|
-
expandPastedTextTokens,
|
|
16
15
|
imageReferenceIds,
|
|
17
16
|
pastedTextReferenceIds,
|
|
18
17
|
} from '../paste-attachments.mjs';
|
|
@@ -489,15 +488,14 @@ export function createPromptSubmit({
|
|
|
489
488
|
const textSnapshot = Object.fromEntries(Object.entries(pastedTextsRef.current || {})
|
|
490
489
|
.filter(([id]) => textRefs.has(Number(id))));
|
|
491
490
|
const hasTextSnapshot = Object.keys(textSnapshot).length > 0;
|
|
492
|
-
const
|
|
493
|
-
const
|
|
491
|
+
const content = buildPromptContentWithImages(text, imageSnapshot);
|
|
492
|
+
const imageRestoreMeta = Object.fromEntries(Object.entries(imageSnapshot).map(([id, image]) => {
|
|
493
|
+
const { content: _content, ...metadata } = image;
|
|
494
|
+
return [id, { ...metadata, sizeBytes: Math.floor((String(image.content || '').length * 3) / 4) }];
|
|
495
|
+
}));
|
|
494
496
|
const accepted = submitPrompt(content, {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
// (pastedTexts entries are cleared on accept). History recall therefore
|
|
498
|
-
// shows the full original text instead of the token — acceptable.
|
|
499
|
-
displayText: expandedText,
|
|
500
|
-
pastedImages: imageSnapshot,
|
|
497
|
+
...((hasImageSnapshot || hasTextSnapshot) ? { displayText: text } : {}),
|
|
498
|
+
pastedImages: imageRestoreMeta,
|
|
501
499
|
pastedTexts: textSnapshot,
|
|
502
500
|
onCommitted: (hasImageSnapshot || hasTextSnapshot)
|
|
503
501
|
? () => { clearPastedImagesSnapshot(imageSnapshot); clearPastedTextsSnapshot(textSnapshot); }
|
|
@@ -83,7 +83,11 @@ export function usePromptHandlers({
|
|
|
83
83
|
return Promise.all(chunks.map(async (chunk) => {
|
|
84
84
|
if (!chunk.imagePath) return chunk.text;
|
|
85
85
|
try {
|
|
86
|
-
const image = await readImageAttachmentFromPath(
|
|
86
|
+
const image = await readImageAttachmentFromPath(
|
|
87
|
+
chunk.text,
|
|
88
|
+
state.cwd || process.cwd(),
|
|
89
|
+
{ provider: state.provider || '' },
|
|
90
|
+
);
|
|
87
91
|
if (!image) return chunk.text;
|
|
88
92
|
const ref = registerPastedImage(image);
|
|
89
93
|
showPromptHint(`attached ${image.filename || 'image'}`, 'plain');
|
|
@@ -123,7 +127,7 @@ export function usePromptHandlers({
|
|
|
123
127
|
.then((clip) => {
|
|
124
128
|
const normalized = String(clip ?? '').replace(/\r\n?/g, '\n');
|
|
125
129
|
if (normalized) return processText(normalized, true);
|
|
126
|
-
return readClipboardImageAttachment()
|
|
130
|
+
return readClipboardImageAttachment({ provider: state.provider || '' })
|
|
127
131
|
.then((image) => {
|
|
128
132
|
if (!image) {
|
|
129
133
|
showPromptHint('no text or image found on clipboard', 'plain');
|
|
@@ -141,7 +145,7 @@ export function usePromptHandlers({
|
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
return processText(value);
|
|
144
|
-
}, [registerPastedImage, registerPastedText, showPromptHint, state.cwd]);
|
|
148
|
+
}, [registerPastedImage, registerPastedText, showPromptHint, state.cwd, state.provider]);
|
|
145
149
|
|
|
146
150
|
const handlePromptHistoryNavigate = useCallback((direction, currentText = '', meta = {}) => {
|
|
147
151
|
const currentValue = String(currentText || '');
|