mixdog 0.9.67 → 0.9.69
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 +6 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -6
- package/src/runtime/agent/orchestrator/context/collect.mjs +42 -27
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +0 -6
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +161 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +8 -204
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +25 -13
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +0 -11
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +613 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +10 -2
- package/src/runtime/agent/orchestrator/session/store.mjs +9 -575
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +41 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +154 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +78 -18
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +9 -144
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -3
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +24 -8
- package/src/runtime/channels/lib/config.mjs +6 -5
- package/src/runtime/channels/lib/owned-runtime.mjs +65 -5
- package/src/runtime/channels/lib/scheduler.mjs +7 -15
- package/src/runtime/channels/lib/tool-dispatch.mjs +6 -1
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +6 -2
- package/src/runtime/channels/lib/webhook.mjs +36 -46
- package/src/runtime/channels/lib/worker-main.mjs +45 -92
- package/src/runtime/shared/automation-attachments.mjs +72 -0
- package/src/runtime/shared/automation-workflow.mjs +41 -0
- package/src/runtime/shared/schedule-session-run.mjs +10 -1
- package/src/runtime/shared/schedules-db.mjs +18 -3
- package/src/runtime/shared/webhook-session-run.mjs +57 -0
- package/src/runtime/shared/webhooks-db.mjs +19 -3
- package/src/session-runtime/channel-config-api.mjs +8 -1
- package/src/session-runtime/lifecycle-api.mjs +7 -0
- package/src/session-runtime/memory-daemon-probe.mjs +61 -0
- package/src/session-runtime/model-capabilities.mjs +6 -4
- package/src/session-runtime/notification-bus.mjs +82 -0
- package/src/session-runtime/provider-auth-api.mjs +16 -1
- package/src/session-runtime/provider-usage.mjs +3 -0
- package/src/session-runtime/remote-control.mjs +166 -0
- package/src/session-runtime/remote-transcript.mjs +126 -0
- package/src/session-runtime/remote-transition-queue.mjs +14 -0
- package/src/session-runtime/runtime-core.mjs +184 -660
- package/src/session-runtime/runtime-tunables.mjs +57 -0
- package/src/session-runtime/self-update.mjs +129 -0
- package/src/session-runtime/skills-api.mjs +77 -0
- package/src/session-runtime/tool-surface.mjs +83 -0
- package/src/session-runtime/workflow-agents-api.mjs +206 -3
- package/src/session-runtime/workflow.mjs +84 -17
- package/src/standalone/agent-tool/worker-index.mjs +287 -0
- package/src/standalone/agent-tool.mjs +22 -346
- package/src/standalone/agent-watchdog-registry.mjs +101 -0
- package/src/standalone/channel-admin.mjs +36 -1
- package/src/standalone/channel-daemon-client.mjs +5 -1
- package/src/standalone/channel-daemon-transport.mjs +112 -20
- package/src/standalone/channel-daemon.mjs +6 -4
- package/src/standalone/channel-worker.mjs +7 -13
- package/src/tui/App.jsx +2 -32
- package/src/tui/app/channel-pickers.mjs +2 -143
- package/src/tui/app/slash-commands.mjs +1 -3
- package/src/tui/app/slash-dispatch.mjs +3 -3
- package/src/tui/components/StatusLine.jsx +6 -5
- package/src/tui/dist/index.mjs +162 -259
- package/src/tui/engine/labels.mjs +0 -8
- package/src/tui/engine/session-api-ext.mjs +45 -10
- package/src/tui/engine/turn-watchdog.mjs +92 -0
- package/src/tui/engine/turn.mjs +18 -70
- package/src/tui/engine.mjs +14 -1
- package/src/workflows/default/WORKFLOW.md +1 -1
- package/src/workflows/solo/WORKFLOW.md +1 -1
|
@@ -10,6 +10,8 @@ import { loadConfig } from '../agent/orchestrator/config.mjs';
|
|
|
10
10
|
import { createSession } from '../agent/orchestrator/session/manager/session-lifecycle.mjs';
|
|
11
11
|
import { askSession } from '../agent/orchestrator/session/manager/ask-session.mjs';
|
|
12
12
|
import { parseScheduleModelRef } from './schedule-model-ref.mjs';
|
|
13
|
+
import { automationWorkflowOpts } from './automation-workflow.mjs';
|
|
14
|
+
import { automationPromptContent } from './automation-attachments.mjs';
|
|
13
15
|
|
|
14
16
|
/** Resolve schedule.model into a {provider,model,effort?,fast?} route. */
|
|
15
17
|
function scheduleRouteFromModelRef(modelRef, config = null) {
|
|
@@ -43,6 +45,9 @@ export async function runScheduleSession(schedule, { config = null, prompt: prom
|
|
|
43
45
|
}
|
|
44
46
|
const route = scheduleRouteFromModelRef(schedule.model, config);
|
|
45
47
|
const cwd = schedule.cwd ? String(schedule.cwd) : null;
|
|
48
|
+
// Every fire is a fresh New task (user decision): one prompt, the
|
|
49
|
+
// schedule's model/workflow/project, a brand-new session in the sidebar
|
|
50
|
+
// Automations section (newest per name wins there).
|
|
46
51
|
const session = createSession({
|
|
47
52
|
provider: route.provider,
|
|
48
53
|
model: route.model,
|
|
@@ -51,13 +56,17 @@ export async function runScheduleSession(schedule, { config = null, prompt: prom
|
|
|
51
56
|
owner: 'user',
|
|
52
57
|
sourceType: 'schedule',
|
|
53
58
|
sourceName: schedule.name,
|
|
59
|
+
sourceDelivery: schedule.delivery || null,
|
|
54
60
|
...(cwd ? { cwd } : {}),
|
|
55
61
|
desktopSession: cwd
|
|
56
62
|
? { classification: 'project', projectPath: cwd }
|
|
57
63
|
: { classification: 'task', projectPath: null },
|
|
64
|
+
...automationWorkflowOpts(schedule.workflow),
|
|
58
65
|
});
|
|
59
66
|
// The user message is the schedule's instructions verbatim: the desktop
|
|
60
67
|
// title/preview derive from it, so no "[Scheduled task: …]" header noise.
|
|
61
|
-
|
|
68
|
+
// Stored attachments ride along as composer-style content parts.
|
|
69
|
+
const content = automationPromptContent(prompt, schedule.attachments);
|
|
70
|
+
const result = await askSession(session.id, content, null, null, cwd || undefined);
|
|
62
71
|
return { sessionId: session.id, result: String(result?.content || '') };
|
|
63
72
|
}
|
|
@@ -33,6 +33,9 @@ CREATE TABLE IF NOT EXISTS scheduler.schedules (
|
|
|
33
33
|
channel_id text,
|
|
34
34
|
model text,
|
|
35
35
|
cwd text,
|
|
36
|
+
workflow text,
|
|
37
|
+
attachments jsonb,
|
|
38
|
+
delivery text,
|
|
36
39
|
prompt text NOT NULL,
|
|
37
40
|
enabled boolean NOT NULL DEFAULT true,
|
|
38
41
|
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','done')),
|
|
@@ -45,6 +48,9 @@ CREATE TABLE IF NOT EXISTS scheduler.schedules (
|
|
|
45
48
|
CONSTRAINT schedules_when_xor CHECK ((when_at IS NOT NULL) <> (when_cron IS NOT NULL))
|
|
46
49
|
);
|
|
47
50
|
ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS cwd text;
|
|
51
|
+
ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS workflow text;
|
|
52
|
+
ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS attachments jsonb;
|
|
53
|
+
ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS delivery text;
|
|
48
54
|
`;
|
|
49
55
|
|
|
50
56
|
async function getDb(dataDir = resolvePluginData()) {
|
|
@@ -82,6 +88,9 @@ function rowToDef(row) {
|
|
|
82
88
|
channelId: row.channel_id,
|
|
83
89
|
model: row.model,
|
|
84
90
|
cwd: row.cwd,
|
|
91
|
+
workflow: row.workflow,
|
|
92
|
+
attachments: row.attachments || null,
|
|
93
|
+
delivery: row.delivery || null,
|
|
85
94
|
prompt: row.prompt,
|
|
86
95
|
enabled: row.enabled,
|
|
87
96
|
status: row.status,
|
|
@@ -94,7 +103,7 @@ function rowToDef(row) {
|
|
|
94
103
|
};
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
const COLS = 'name, description, when_at, when_cron, timezone, target, channel_id, model, cwd, prompt, enabled, status, last_fired_at, next_fire_at, deferred_until, skipped_until, created_at, updated_at';
|
|
106
|
+
const COLS = 'name, description, when_at, when_cron, timezone, target, channel_id, model, cwd, workflow, attachments, delivery, prompt, enabled, status, last_fired_at, next_fire_at, deferred_until, skipped_until, created_at, updated_at';
|
|
98
107
|
|
|
99
108
|
// ---------------------------------------------------------------------------
|
|
100
109
|
// Public API
|
|
@@ -130,6 +139,9 @@ export async function upsertSchedule(def, { dataDir } = {}) {
|
|
|
130
139
|
def.channelId ?? null,
|
|
131
140
|
def.model ?? null,
|
|
132
141
|
def.cwd ?? null,
|
|
142
|
+
def.workflow ?? null,
|
|
143
|
+
def.attachments ? JSON.stringify(def.attachments) : null,
|
|
144
|
+
def.delivery ?? null,
|
|
133
145
|
def.prompt,
|
|
134
146
|
def.enabled ?? true,
|
|
135
147
|
def.status ?? 'active',
|
|
@@ -137,8 +149,8 @@ export async function upsertSchedule(def, { dataDir } = {}) {
|
|
|
137
149
|
];
|
|
138
150
|
const { rows } = await db.query(
|
|
139
151
|
`INSERT INTO scheduler.schedules
|
|
140
|
-
(name, description, when_at, when_cron, timezone, target, channel_id, model, cwd, prompt, enabled, status, next_fire_at)
|
|
141
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
|
152
|
+
(name, description, when_at, when_cron, timezone, target, channel_id, model, cwd, workflow, attachments, delivery, prompt, enabled, status, next_fire_at)
|
|
153
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
|
|
142
154
|
ON CONFLICT (name) DO UPDATE SET
|
|
143
155
|
description = EXCLUDED.description,
|
|
144
156
|
when_at = EXCLUDED.when_at,
|
|
@@ -148,6 +160,9 @@ export async function upsertSchedule(def, { dataDir } = {}) {
|
|
|
148
160
|
channel_id = EXCLUDED.channel_id,
|
|
149
161
|
model = EXCLUDED.model,
|
|
150
162
|
cwd = EXCLUDED.cwd,
|
|
163
|
+
workflow = EXCLUDED.workflow,
|
|
164
|
+
attachments = EXCLUDED.attachments,
|
|
165
|
+
delivery = EXCLUDED.delivery,
|
|
151
166
|
prompt = EXCLUDED.prompt,
|
|
152
167
|
enabled = EXCLUDED.enabled,
|
|
153
168
|
next_fire_at = EXCLUDED.next_fire_at,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run an inbound webhook as a VISIBLE Mixdog session (schedules parity, user
|
|
3
|
+
* decision): no Lead-context injection, no channel forward — every fire is a
|
|
4
|
+
* fresh New task with the endpoint's prompt/model/workflow/project, owned by
|
|
5
|
+
* 'user' + sourceType 'webhook' (sidebar Automations section, newest per
|
|
6
|
+
* name). Invoked from the channels worker's webhook dispatch.
|
|
7
|
+
*/
|
|
8
|
+
import { loadConfig } from '../agent/orchestrator/config.mjs';
|
|
9
|
+
import { createSession } from '../agent/orchestrator/session/manager/session-lifecycle.mjs';
|
|
10
|
+
import { askSession } from '../agent/orchestrator/session/manager/ask-session.mjs';
|
|
11
|
+
import { parseScheduleModelRef } from './schedule-model-ref.mjs';
|
|
12
|
+
import { automationWorkflowOpts } from './automation-workflow.mjs';
|
|
13
|
+
import { automationPromptContent } from './automation-attachments.mjs';
|
|
14
|
+
|
|
15
|
+
/** Endpoint model ref wins; the maintenance.webhook route is the fallback. */
|
|
16
|
+
function webhookRoute(modelRef) {
|
|
17
|
+
if (modelRef) {
|
|
18
|
+
const ref = parseScheduleModelRef(modelRef);
|
|
19
|
+
if (ref && typeof ref === 'object') return ref;
|
|
20
|
+
}
|
|
21
|
+
const cfg = loadConfig({ secrets: false });
|
|
22
|
+
const maintenance = cfg?.maintenance?.webhook;
|
|
23
|
+
if (maintenance?.provider && maintenance?.model) {
|
|
24
|
+
return { provider: maintenance.provider, model: maintenance.model };
|
|
25
|
+
}
|
|
26
|
+
throw new Error('webhook run has no model: set one on the endpoint or configure maintenance.webhook');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function runWebhookSession({ name, model = null, prompt, cwd = null, workflow = null, attachments = null, delivery = null }) {
|
|
30
|
+
const endpoint = String(name || '').trim();
|
|
31
|
+
const body = String(prompt || '').trim();
|
|
32
|
+
if (!endpoint) throw new Error('runWebhookSession: endpoint name required');
|
|
33
|
+
if (!body) throw new Error(`webhook "${endpoint}" has no prompt body`);
|
|
34
|
+
const route = webhookRoute(model);
|
|
35
|
+
const projectCwd = cwd ? String(cwd) : null;
|
|
36
|
+
const session = createSession({
|
|
37
|
+
provider: route.provider,
|
|
38
|
+
model: route.model,
|
|
39
|
+
...(route.effort ? { effort: route.effort } : {}),
|
|
40
|
+
...(route.fast === true ? { fast: true } : {}),
|
|
41
|
+
owner: 'user',
|
|
42
|
+
sourceType: 'webhook',
|
|
43
|
+
sourceName: endpoint,
|
|
44
|
+
sourceDelivery: delivery || null,
|
|
45
|
+
...(projectCwd ? { cwd: projectCwd } : {}),
|
|
46
|
+
desktopSession: projectCwd
|
|
47
|
+
? { classification: 'project', projectPath: projectCwd }
|
|
48
|
+
: { classification: 'task', projectPath: null },
|
|
49
|
+
...automationWorkflowOpts(workflow),
|
|
50
|
+
});
|
|
51
|
+
// The user message leads with the endpoint's instructions, so Recent titles
|
|
52
|
+
// read as the user-authored intent rather than payload noise.
|
|
53
|
+
// Stored attachments ride along as composer-style content parts.
|
|
54
|
+
const content = automationPromptContent(body, attachments);
|
|
55
|
+
const result = await askSession(session.id, content, null, null, projectCwd || undefined);
|
|
56
|
+
return { sessionId: session.id, result: String(result?.content || '') };
|
|
57
|
+
}
|
|
@@ -40,6 +40,10 @@ CREATE TABLE IF NOT EXISTS webhooks.endpoints (
|
|
|
40
40
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
41
41
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
42
42
|
);
|
|
43
|
+
ALTER TABLE webhooks.endpoints ADD COLUMN IF NOT EXISTS cwd text;
|
|
44
|
+
ALTER TABLE webhooks.endpoints ADD COLUMN IF NOT EXISTS workflow text;
|
|
45
|
+
ALTER TABLE webhooks.endpoints ADD COLUMN IF NOT EXISTS attachments jsonb;
|
|
46
|
+
ALTER TABLE webhooks.endpoints ADD COLUMN IF NOT EXISTS delivery text;
|
|
43
47
|
CREATE TABLE IF NOT EXISTS webhooks.deliveries (
|
|
44
48
|
endpoint text NOT NULL,
|
|
45
49
|
delivery_id text NOT NULL,
|
|
@@ -77,7 +81,7 @@ async function getDb(dataDir = resolvePluginData()) {
|
|
|
77
81
|
// Row <-> def mapping
|
|
78
82
|
// ---------------------------------------------------------------------------
|
|
79
83
|
|
|
80
|
-
const ENDPOINT_COLS = 'name, description, channel_id, role, model, parser, secret, instructions, enabled, created_at, updated_at';
|
|
84
|
+
const ENDPOINT_COLS = 'name, description, channel_id, role, model, parser, secret, cwd, workflow, attachments, delivery, instructions, enabled, created_at, updated_at';
|
|
81
85
|
|
|
82
86
|
function rowToEndpoint(row) {
|
|
83
87
|
if (!row) return null;
|
|
@@ -88,6 +92,10 @@ function rowToEndpoint(row) {
|
|
|
88
92
|
role: row.role,
|
|
89
93
|
model: row.model,
|
|
90
94
|
parser: row.parser,
|
|
95
|
+
cwd: row.cwd,
|
|
96
|
+
workflow: row.workflow,
|
|
97
|
+
attachments: row.attachments || null,
|
|
98
|
+
delivery: row.delivery || null,
|
|
91
99
|
// Never project the plaintext secret through list/load config paths;
|
|
92
100
|
// callers get a presence flag and must fetch the value via
|
|
93
101
|
// readEndpointSecret (the single, explicit secret-read path).
|
|
@@ -165,13 +173,17 @@ export async function upsertEndpoint(def, { dataDir } = {}) {
|
|
|
165
173
|
def.model ?? null,
|
|
166
174
|
def.parser ?? null,
|
|
167
175
|
def.secret ?? null,
|
|
176
|
+
def.cwd ?? null,
|
|
177
|
+
def.workflow ?? null,
|
|
178
|
+
def.attachments ? JSON.stringify(def.attachments) : null,
|
|
179
|
+
def.delivery ?? null,
|
|
168
180
|
def.instructions ?? '',
|
|
169
181
|
def.enabled ?? true,
|
|
170
182
|
];
|
|
171
183
|
const { rows } = await db.query(
|
|
172
184
|
`INSERT INTO webhooks.endpoints
|
|
173
|
-
(name, description, channel_id, role, model, parser, secret, instructions, enabled)
|
|
174
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
|
185
|
+
(name, description, channel_id, role, model, parser, secret, cwd, workflow, attachments, delivery, instructions, enabled)
|
|
186
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
|
175
187
|
ON CONFLICT (name) DO UPDATE SET
|
|
176
188
|
description = EXCLUDED.description,
|
|
177
189
|
channel_id = EXCLUDED.channel_id,
|
|
@@ -179,6 +191,10 @@ export async function upsertEndpoint(def, { dataDir } = {}) {
|
|
|
179
191
|
model = EXCLUDED.model,
|
|
180
192
|
parser = EXCLUDED.parser,
|
|
181
193
|
secret = EXCLUDED.secret,
|
|
194
|
+
cwd = EXCLUDED.cwd,
|
|
195
|
+
workflow = EXCLUDED.workflow,
|
|
196
|
+
attachments = EXCLUDED.attachments,
|
|
197
|
+
delivery = EXCLUDED.delivery,
|
|
182
198
|
instructions = EXCLUDED.instructions,
|
|
183
199
|
enabled = EXCLUDED.enabled,
|
|
184
200
|
updated_at = now()
|
|
@@ -17,7 +17,7 @@ import { runScheduleSession } from '../runtime/shared/schedule-session-run.mjs';
|
|
|
17
17
|
// API object; the mutating admin helpers are imported directly here and the
|
|
18
18
|
// runtime injects only the closure-owned callbacks (backend flush, channel
|
|
19
19
|
// worker handle, soft reload).
|
|
20
|
-
export function createChannelConfigApi({ flushBackendSave, channels, reloadChannelsSoon }) {
|
|
20
|
+
export function createChannelConfigApi({ flushBackendSave, channels, reloadChannelsSoon, ensureAutomationRuntime = () => {} }) {
|
|
21
21
|
return {
|
|
22
22
|
async getChannelSetup() {
|
|
23
23
|
// Flush a pending debounced backend switch first so setup readers
|
|
@@ -42,6 +42,10 @@ export function createChannelConfigApi({ flushBackendSave, channels, reloadChann
|
|
|
42
42
|
async saveSchedule(entry) {
|
|
43
43
|
const result = await saveSchedule(entry);
|
|
44
44
|
reloadChannelsSoon();
|
|
45
|
+
// First automation created while the app is already running: the boot-
|
|
46
|
+
// time autostart check has passed, so kick the worker start path now
|
|
47
|
+
// (no-op when it is already up).
|
|
48
|
+
ensureAutomationRuntime();
|
|
45
49
|
return result;
|
|
46
50
|
},
|
|
47
51
|
async deleteSchedule(name) {
|
|
@@ -52,11 +56,13 @@ export function createChannelConfigApi({ flushBackendSave, channels, reloadChann
|
|
|
52
56
|
async setScheduleEnabled(name, enabled) {
|
|
53
57
|
const result = await setScheduleEnabled(name, enabled);
|
|
54
58
|
reloadChannelsSoon();
|
|
59
|
+
if (enabled !== false) ensureAutomationRuntime();
|
|
55
60
|
return result;
|
|
56
61
|
},
|
|
57
62
|
async saveWebhook(entry) {
|
|
58
63
|
const result = await saveWebhook(entry);
|
|
59
64
|
reloadChannelsSoon();
|
|
65
|
+
ensureAutomationRuntime();
|
|
60
66
|
return result;
|
|
61
67
|
},
|
|
62
68
|
async deleteWebhook(name) {
|
|
@@ -67,6 +73,7 @@ export function createChannelConfigApi({ flushBackendSave, channels, reloadChann
|
|
|
67
73
|
async setWebhookEnabled(name, enabled) {
|
|
68
74
|
const result = await setWebhookEnabled(name, enabled);
|
|
69
75
|
reloadChannelsSoon();
|
|
76
|
+
if (enabled !== false) ensureAutomationRuntime();
|
|
70
77
|
return result;
|
|
71
78
|
},
|
|
72
79
|
// Read-only secret fetch for the webhook editor; no worker reload.
|
|
@@ -69,6 +69,9 @@ export function createLifecycleApi(deps) {
|
|
|
69
69
|
// Recent / TUI resume next to lead sessions instead of hiding like
|
|
70
70
|
// agent dispatches.
|
|
71
71
|
|| sourceType === 'schedule'
|
|
72
|
+
// Webhook fires run as visible sessions too (user decision: no Lead
|
|
73
|
+
// injection — the session row IS the notification).
|
|
74
|
+
|| sourceType === 'webhook'
|
|
72
75
|
|| (!sourceType && !sourceName && !isAgentOwner(owner));
|
|
73
76
|
if (!leadish) return null;
|
|
74
77
|
const rawPreview = s.preview || '';
|
|
@@ -106,6 +109,10 @@ export function createLifecycleApi(deps) {
|
|
|
106
109
|
// after a turn had already finished.
|
|
107
110
|
heartbeatAt: Number(heartbeatMtimes.get(s.id)) || 0,
|
|
108
111
|
desktopSession: s.desktopSession || null,
|
|
112
|
+
// Automation origin: lets the desktop group schedule/webhook runner
|
|
113
|
+
// sessions under the sidebar Automations section instead of Recent.
|
|
114
|
+
sourceType: sourceType || null,
|
|
115
|
+
sourceName: clean(s.sourceName || '') || null,
|
|
109
116
|
};
|
|
110
117
|
}).filter(Boolean);
|
|
111
118
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Eager memory-daemon boot for remote sessions. The channels worker forwards
|
|
2
|
+
// transcript ingests to the memory HTTP service, so its port must be published
|
|
3
|
+
// (and actually listening) before the worker sends its first ingest — otherwise
|
|
4
|
+
// early channel traffic finds no port and gets buffered. Extracted from
|
|
5
|
+
// runtime-core's startRemote, which now just fires it.
|
|
6
|
+
import * as httpMod from 'node:http';
|
|
7
|
+
|
|
8
|
+
const HEALTH_ATTEMPTS = 30;
|
|
9
|
+
const HEALTH_RETRY_MS = 500;
|
|
10
|
+
const HEALTH_TIMEOUT_MS = 1500;
|
|
11
|
+
|
|
12
|
+
function healthOk(port) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const request = httpMod.request(
|
|
15
|
+
{ hostname: '127.0.0.1', port, path: '/health', timeout: HEALTH_TIMEOUT_MS },
|
|
16
|
+
(response) => {
|
|
17
|
+
let body = '';
|
|
18
|
+
response.on('data', (chunk) => { body += chunk; });
|
|
19
|
+
response.on('end', () => {
|
|
20
|
+
try { resolve(JSON.parse(body)?.status === 'ok'); } catch { resolve(false); }
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
request.on('error', () => resolve(false));
|
|
25
|
+
request.on('timeout', () => { request.destroy(); resolve(false); });
|
|
26
|
+
request.end();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Detached, never throws: resolving the module only hands back a handle, so
|
|
31
|
+
* this awaits start() and then polls /health until the daemon is provably
|
|
32
|
+
* reachable (or the failure is profiled). */
|
|
33
|
+
export function startMemoryDaemonEagerly({ getMemoryModule, bootProfile }) {
|
|
34
|
+
void (async () => {
|
|
35
|
+
try {
|
|
36
|
+
// Yield one tick first: the module resolve + daemon fork below is a long
|
|
37
|
+
// synchronous chain that would otherwise starve Ink's queued render and
|
|
38
|
+
// keypress handling.
|
|
39
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
40
|
+
const module = await getMemoryModule();
|
|
41
|
+
const started = typeof module?.start === 'function' ? await module.start() : null;
|
|
42
|
+
const port = started?.port;
|
|
43
|
+
if (!port) {
|
|
44
|
+
bootProfile('channels:memory-eager-init-failed', { error: 'no port from start()' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
for (let attempt = 0; attempt < HEALTH_ATTEMPTS; attempt += 1) {
|
|
48
|
+
try {
|
|
49
|
+
if (await healthOk(port)) {
|
|
50
|
+
bootProfile('channels:memory-eager-init-ready', { port });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
} catch { /* probe failure is retried below */ }
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, HEALTH_RETRY_MS));
|
|
55
|
+
}
|
|
56
|
+
bootProfile('channels:memory-eager-init-failed', { error: `health not ok after retries (port ${port})` });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
bootProfile('channels:memory-eager-init-failed', { error: error?.message || String(error) });
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
}
|
|
@@ -103,6 +103,8 @@ export function fastPreferenceFor(config, provider, model) {
|
|
|
103
103
|
if (!key) return false;
|
|
104
104
|
const saved = config?.modelSettings?.[key];
|
|
105
105
|
if (saved && typeof saved === 'object' && hasOwn(saved, 'fast')) return saved.fast === true;
|
|
106
|
+
// Pre-modelSettings configs kept the preference in a parallel `fastModels`
|
|
107
|
+
// map. Reading it keeps those installs working; nothing writes it anymore.
|
|
106
108
|
return config?.fastModels?.[key] === true;
|
|
107
109
|
}
|
|
108
110
|
|
|
@@ -118,11 +120,11 @@ export function saveModelSettings(cfgMod, route, { fastCapable = true, baseConfi
|
|
|
118
120
|
else nextSetting.fast = false;
|
|
119
121
|
modelSettings[key] = nextSetting;
|
|
120
122
|
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
+
// modelSettings is the single source of truth for the fast preference; the
|
|
124
|
+
// stale mirror in `fastModels` is dropped for the key being written so the
|
|
125
|
+
// two can never disagree.
|
|
123
126
|
const fastModels = { ...(nextConfig.fastModels || {}) };
|
|
124
|
-
|
|
125
|
-
else delete fastModels[key];
|
|
127
|
+
delete fastModels[key];
|
|
126
128
|
|
|
127
129
|
const savedConfig = { ...nextConfig, modelSettings, fastModels };
|
|
128
130
|
cfgMod.saveConfig(savedConfig);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Runtime notification fan-out. A notification reaches listeners (TUI/API) and,
|
|
2
|
+
// for terminal tool completions, may also be mirrored into the session's
|
|
3
|
+
// pending queue so the model sees it next turn. The delivered registry keeps
|
|
4
|
+
// those two paths from double-injecting the same completion.
|
|
5
|
+
import { modelVisibleToolCompletionMessage } from '../runtime/shared/tool-execution-contract.mjs';
|
|
6
|
+
import { markCompletionEntry } from '../runtime/agent/orchestrator/session/manager/pending-messages.mjs';
|
|
7
|
+
import {
|
|
8
|
+
isDeliveredCompletion,
|
|
9
|
+
logDuplicateSkip,
|
|
10
|
+
recordDeliveredCompletion,
|
|
11
|
+
} from '../runtime/agent/orchestrator/session/manager/delivered-completions.mjs';
|
|
12
|
+
import { shouldMirrorCompletionToPendingQueue } from './runtime-tool-routing.mjs';
|
|
13
|
+
|
|
14
|
+
export function createNotificationBus({ listeners, mgr }) {
|
|
15
|
+
function emitRuntimeNotification(content, meta = {}) {
|
|
16
|
+
const text = String(content || '').trim();
|
|
17
|
+
if (!text) return { handled: false, modelVisibleDelivered: false };
|
|
18
|
+
const event = { content: text, meta: meta && typeof meta === 'object' ? meta : {} };
|
|
19
|
+
let handled = false;
|
|
20
|
+
for (const listener of [...listeners]) {
|
|
21
|
+
try {
|
|
22
|
+
if (listener(event) === true) handled = true;
|
|
23
|
+
} catch { /* a broken listener never blocks the rest */ }
|
|
24
|
+
}
|
|
25
|
+
// EXPLICIT model-visible ack: only the TUI execution-ui path sets
|
|
26
|
+
// event.modelVisibleDelivered when it enqueues the body into the active
|
|
27
|
+
// loop. A display-only listener returning true is NOT that ack.
|
|
28
|
+
return { handled, modelVisibleDelivered: event.modelVisibleDelivered === true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Enqueue the model-visible twin unless it was already delivered; returns
|
|
32
|
+
// true when the caller may treat the completion as delivered.
|
|
33
|
+
function enqueueCompletion(callerSessionId, text, meta, path) {
|
|
34
|
+
const visible = modelVisibleToolCompletionMessage(text, meta);
|
|
35
|
+
// Non-completion notifications yield '' here and are never queued.
|
|
36
|
+
if (!visible) return false;
|
|
37
|
+
if (isDeliveredCompletion({ executionId: meta?.execution_id, text: visible })) {
|
|
38
|
+
// Delivered + ACKed on the TUI path: suppress the duplicate but report
|
|
39
|
+
// DELIVERED so the caller stops retrying.
|
|
40
|
+
logDuplicateSkip(path, { executionId: meta?.execution_id, text: visible });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return mgr.enqueuePendingMessage(callerSessionId, markCompletionEntry(visible)) > 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function notifyFnForSession(callerSessionId) {
|
|
47
|
+
return (text, meta = {}) => {
|
|
48
|
+
const { handled, modelVisibleDelivered } = emitRuntimeNotification(text, meta);
|
|
49
|
+
let enqueued = false;
|
|
50
|
+
// Record the TUI delivery so a racing enqueue in THIS process (background
|
|
51
|
+
// reconcile / fallback / notify) skips instead of double-injecting.
|
|
52
|
+
if (modelVisibleDelivered) {
|
|
53
|
+
try {
|
|
54
|
+
recordDeliveredCompletion({
|
|
55
|
+
executionId: meta?.execution_id,
|
|
56
|
+
text: modelVisibleToolCompletionMessage(text, meta),
|
|
57
|
+
});
|
|
58
|
+
} catch { /* registry is best-effort */ }
|
|
59
|
+
}
|
|
60
|
+
// TUI sessions consume raw envelopes for UI cards, but those are
|
|
61
|
+
// internal-only in pending drain: mirror the model-visible twin unless the
|
|
62
|
+
// TUI explicitly acked it.
|
|
63
|
+
if (shouldMirrorCompletionToPendingQueue({
|
|
64
|
+
callerSessionId,
|
|
65
|
+
modelVisibleDelivered,
|
|
66
|
+
hasEnqueue: typeof mgr.enqueuePendingMessage === 'function',
|
|
67
|
+
text,
|
|
68
|
+
meta,
|
|
69
|
+
})) {
|
|
70
|
+
try { enqueued = enqueueCompletion(callerSessionId, text, meta, 'mirror'); } catch { /* best-effort */ }
|
|
71
|
+
}
|
|
72
|
+
// Headless/API listeners may exist without consuming the event: keep the
|
|
73
|
+
// fallback enqueue for an otherwise unhandled completion.
|
|
74
|
+
if (!enqueued && !handled && callerSessionId && typeof mgr.enqueuePendingMessage === 'function') {
|
|
75
|
+
try { enqueued = enqueueCompletion(callerSessionId, text, meta, 'fallback'); } catch { /* best-effort */ }
|
|
76
|
+
}
|
|
77
|
+
return enqueued || handled;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { emitRuntimeNotification, notifyFnForSession };
|
|
82
|
+
}
|
|
@@ -24,6 +24,8 @@ export function createProviderAuthApi({
|
|
|
24
24
|
displayConfig,
|
|
25
25
|
reloadFullConfig,
|
|
26
26
|
awaitKeychainPrewarm,
|
|
27
|
+
isKeychainPrewarmReady = () => true,
|
|
28
|
+
hasProviderSetupCached = () => true,
|
|
27
29
|
invalidateProviderCaches,
|
|
28
30
|
warmProviderModelCache,
|
|
29
31
|
refreshProviderCatalogs,
|
|
@@ -56,8 +58,21 @@ export function createProviderAuthApi({
|
|
|
56
58
|
return renderProviderStatus(displayConfig());
|
|
57
59
|
},
|
|
58
60
|
async getProviderSetup(options = {}) {
|
|
59
|
-
await awaitKeychainPrewarm();
|
|
60
61
|
const force = options?.force === true || options?.refresh === true;
|
|
62
|
+
// An unforced read never blocks: the authoritative setup waits on the OS
|
|
63
|
+
// keychain AND probes local provider ports, which together take seconds on
|
|
64
|
+
// a cold start and used to stall the whole settings sweep behind it. Serve
|
|
65
|
+
// the no-secrets snapshot until the real one is cached — flagged, so the
|
|
66
|
+
// caller shows "checking" instead of a wrong "not connected" — and let the
|
|
67
|
+
// scheduled warmup publish the authoritative result for the next read.
|
|
68
|
+
if (!force && (!isKeychainPrewarmReady() || !hasProviderSetupCached())) {
|
|
69
|
+
const quick = await cachedProviderSetup({ quick: true });
|
|
70
|
+
void Promise.resolve(awaitKeychainPrewarm())
|
|
71
|
+
.then(() => cachedProviderSetup({}))
|
|
72
|
+
.catch(() => {});
|
|
73
|
+
return { ...quick, pendingSecrets: !isKeychainPrewarmReady() };
|
|
74
|
+
}
|
|
75
|
+
await awaitKeychainPrewarm();
|
|
61
76
|
if (force) reloadFullConfig();
|
|
62
77
|
return await cachedProviderSetup({ force });
|
|
63
78
|
},
|
|
@@ -118,6 +118,9 @@ export function createProviderUsage({
|
|
|
118
118
|
return {
|
|
119
119
|
refreshStatuslineUsageSnapshot,
|
|
120
120
|
cachedProviderSetup,
|
|
121
|
+
// True once a secrets-aware setup is cached: callers that must not block
|
|
122
|
+
// (settings hydration) can serve the quick snapshot until then.
|
|
123
|
+
hasProviderSetupCached: () => Boolean(caches.providerSetupCache.setup),
|
|
121
124
|
getUsageDashboard,
|
|
122
125
|
};
|
|
123
126
|
}
|