mixdog 0.9.24 → 0.9.26
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 +2 -1
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/explore-bench-tmp.mjs +1 -1
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/steering-fold-provenance-test.mjs +71 -0
- package/scripts/tool-efficiency-diag.mjs +1 -1
- package/scripts/webhook-smoke.mjs +46 -53
- package/src/defaults/cycle3-review-prompt.md +11 -4
- package/src/defaults/memory-promote-prompt.md +9 -0
- package/src/defaults/skills/setup/SKILL.md +6 -1
- package/src/rules/lead/02-channels.md +3 -3
- package/src/rules/shared/01-tool.md +8 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +17 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +2 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +51 -15
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +164 -9
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +45 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +7 -3
- package/src/runtime/channels/backends/discord.mjs +10 -3
- package/src/runtime/channels/lib/config.mjs +13 -11
- package/src/runtime/channels/lib/inbound-handler.mjs +45 -1
- package/src/runtime/channels/lib/memory-client.mjs +22 -2
- package/src/runtime/channels/lib/owned-runtime.mjs +1 -1
- package/src/runtime/channels/lib/scheduler.mjs +226 -208
- package/src/runtime/channels/lib/status-snapshot.mjs +16 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +7 -318
- package/src/runtime/channels/lib/webhook.mjs +98 -150
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/memory/index.mjs +50 -25
- package/src/runtime/memory/lib/cycle-scheduler.mjs +20 -2
- package/src/runtime/memory/lib/memory-config-flags.mjs +13 -1
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +59 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +10 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +79 -9
- package/src/runtime/memory/lib/pg/adapter.mjs +6 -1
- package/src/runtime/memory/lib/pg/process.mjs +19 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +125 -14
- package/src/runtime/memory/lib/query-handlers.mjs +102 -0
- package/src/runtime/memory/lib/recall-format.mjs +60 -22
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +20 -6
- package/src/runtime/memory/tool-defs.mjs +1 -1
- package/src/runtime/shared/child-guardian.mjs +2 -2
- package/src/runtime/shared/open-url.mjs +2 -1
- package/src/runtime/shared/schedules-db.mjs +350 -0
- package/src/runtime/shared/service-discovery.mjs +169 -0
- package/src/runtime/shared/spawn-flags.mjs +27 -0
- package/src/runtime/shared/tool-primitives.mjs +3 -1
- package/src/runtime/shared/tool-surface.mjs +19 -3
- package/src/runtime/shared/update-checker.mjs +54 -10
- package/src/runtime/shared/webhooks-db.mjs +405 -0
- package/src/session-runtime/channel-config-api.mjs +13 -13
- package/src/session-runtime/lifecycle-api.mjs +14 -0
- package/src/session-runtime/runtime-core.mjs +39 -20
- package/src/standalone/agent-tool.mjs +42 -11
- package/src/standalone/channel-admin.mjs +173 -121
- package/src/standalone/channel-worker.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +10 -5
- package/src/tui/App.jsx +32 -10
- package/src/tui/app/channel-pickers.mjs +9 -9
- package/src/tui/app/clipboard.mjs +14 -0
- package/src/tui/app/doctor.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +17 -7
- package/src/tui/app/settings-picker.mjs +2 -2
- package/src/tui/app/transcript-window.mjs +37 -1
- package/src/tui/app/use-mouse-input.mjs +19 -6
- package/src/tui/components/ToolExecution.jsx +12 -4
- package/src/tui/display-width.mjs +10 -4
- package/src/tui/dist/index.mjs +166 -58
- package/src/tui/engine/agent-job-feed.mjs +42 -3
- package/src/tui/engine/session-api-ext.mjs +12 -12
- package/src/tui/index.jsx +12 -2
- package/src/ui/statusline-segments.mjs +12 -1
- package/src/workflows/default/WORKFLOW.md +29 -38
- package/src/workflows/solo/WORKFLOW.md +15 -20
- package/vendor/ink/build/display-width.js +5 -4
- package/src/runtime/shared/schedules-store.mjs +0 -82
|
@@ -24,6 +24,20 @@ import {
|
|
|
24
24
|
} from '../runtime/shared/config.mjs';
|
|
25
25
|
import { resolvePluginData } from '../runtime/shared/plugin-paths.mjs';
|
|
26
26
|
import { readMarkdownDocument, serializeFrontmatterDoc } from '../runtime/shared/markdown-frontmatter.mjs';
|
|
27
|
+
import {
|
|
28
|
+
listSchedules as dbListSchedules,
|
|
29
|
+
getSchedule as dbGetSchedule,
|
|
30
|
+
upsertSchedule,
|
|
31
|
+
deleteSchedule as dbDeleteSchedule,
|
|
32
|
+
setEnabled as dbSetEnabled,
|
|
33
|
+
} from '../runtime/shared/schedules-db.mjs';
|
|
34
|
+
import {
|
|
35
|
+
listEndpoints as dbListEndpoints,
|
|
36
|
+
loadEndpointConfig as dbLoadEndpoint,
|
|
37
|
+
upsertEndpoint as dbUpsertEndpoint,
|
|
38
|
+
deleteEndpoint as dbDeleteEndpoint,
|
|
39
|
+
setEndpointEnabled as dbSetEndpointEnabled,
|
|
40
|
+
} from '../runtime/shared/webhooks-db.mjs';
|
|
27
41
|
|
|
28
42
|
const NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
29
43
|
const DEFAULT_CHANNELS = Object.freeze({
|
|
@@ -38,10 +52,6 @@ function dataDir() {
|
|
|
38
52
|
return resolvePluginData();
|
|
39
53
|
}
|
|
40
54
|
|
|
41
|
-
function schedulesDir() {
|
|
42
|
-
return join(dataDir(), 'schedules');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
55
|
function webhooksDir() {
|
|
46
56
|
return join(dataDir(), 'webhooks');
|
|
47
57
|
}
|
|
@@ -279,34 +289,86 @@ function normalizeCron(time) {
|
|
|
279
289
|
return value;
|
|
280
290
|
}
|
|
281
291
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
292
|
+
// Day-name / keyword -> cron day-of-week number (Sun=0 .. Sat=6).
|
|
293
|
+
const DAY_TOKEN_TO_DOW = {
|
|
294
|
+
sun: 0, sunday: 0,
|
|
295
|
+
mon: 1, monday: 1,
|
|
296
|
+
tue: 2, tues: 2, tuesday: 2,
|
|
297
|
+
wed: 3, weds: 3, wednesday: 3,
|
|
298
|
+
thu: 4, thur: 4, thurs: 4, thursday: 4,
|
|
299
|
+
fri: 5, friday: 5,
|
|
300
|
+
sat: 6, saturday: 6,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Fold a legacy `days` selector into the day-of-week (last) field of a cron
|
|
304
|
+
// expression: daily -> '*', weekday -> '1-5', weekend -> '0,6', explicit day
|
|
305
|
+
// lists ("mon,wed,fri" | "1,3,5") -> comma-joined numbers. Throws on an
|
|
306
|
+
// unmappable token so bad combos surface instead of silently mis-scheduling.
|
|
307
|
+
export function foldDaysIntoCron(cron, days) {
|
|
308
|
+
const parts = String(cron).trim().split(/\s+/);
|
|
309
|
+
const dowIndex = parts.length - 1;
|
|
310
|
+
const raw = String(days || '').trim().toLowerCase();
|
|
311
|
+
// days absent -> keep the cron's own day-of-week field ('0 9 * * 1' stays
|
|
312
|
+
// Monday-only). Only an explicit selector rewrites the dow field.
|
|
313
|
+
if (!raw) return parts.join(' ');
|
|
314
|
+
let dow;
|
|
315
|
+
if (raw === 'daily' || raw === 'everyday' || raw === 'every day') dow = '*';
|
|
316
|
+
else if (raw === 'weekday' || raw === 'weekdays') dow = '1-5';
|
|
317
|
+
else if (raw === 'weekend' || raw === 'weekends') dow = '0,6';
|
|
318
|
+
else {
|
|
319
|
+
const nums = raw.split(/[\s,]+/).filter(Boolean).map((tok) => (
|
|
320
|
+
/^[0-6]$/.test(tok) ? Number(tok) : DAY_TOKEN_TO_DOW[tok]
|
|
321
|
+
));
|
|
322
|
+
if (nums.some((n) => n === undefined)) {
|
|
323
|
+
throw new Error(`days "${days}" is not a recognizable day selector`);
|
|
289
324
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
325
|
+
dow = nums.join(',');
|
|
326
|
+
}
|
|
327
|
+
parts[dowIndex] = dow;
|
|
328
|
+
return parts.join(' ');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parseAtDatetime(at) {
|
|
332
|
+
const raw = String(at || '').trim();
|
|
333
|
+
if (!raw) throw new Error('at must be a datetime');
|
|
334
|
+
const d = new Date(raw);
|
|
335
|
+
if (Number.isNaN(d.getTime())) throw new Error(`at "${at}" is not a valid datetime`);
|
|
336
|
+
return d;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Map the store's def shape onto the flat display shape every schedule reader
|
|
340
|
+
// (channelSetup, renderChannelStatus, TUI pickers) consumes: `.time` renders
|
|
341
|
+
// the cron or the one-shot datetime, `.route` the channel/session target.
|
|
342
|
+
function scheduleToDisplay(s) {
|
|
343
|
+
return {
|
|
344
|
+
name: s.name,
|
|
345
|
+
description: s.description || '',
|
|
346
|
+
time: s.whenCron || (s.whenAt ? `at ${new Date(s.whenAt).toISOString()}` : ''),
|
|
347
|
+
whenAt: s.whenAt || undefined,
|
|
348
|
+
whenCron: s.whenCron || undefined,
|
|
349
|
+
timezone: s.timezone || undefined,
|
|
350
|
+
channel: s.channelId || undefined,
|
|
351
|
+
model: s.model || undefined,
|
|
352
|
+
enabled: s.enabled !== false,
|
|
353
|
+
instructions: s.prompt,
|
|
354
|
+
route: s.target === 'channel' ? `channel:${s.channelId}` : 'session',
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function listSchedules() {
|
|
359
|
+
const rows = await dbListSchedules();
|
|
360
|
+
return rows.map(scheduleToDisplay);
|
|
304
361
|
}
|
|
305
362
|
|
|
306
|
-
|
|
363
|
+
// Register or update a schedule in the PG store. Recurring input maps `time`
|
|
364
|
+
// (+ optional `days`) to a cron; one-shot input maps an `at` datetime; the two
|
|
365
|
+
// are mutually exclusive (also enforced by the store's when_at/when_cron XOR).
|
|
366
|
+
// `channel` selects a channel target (model required); otherwise session.
|
|
367
|
+
export async function saveSchedule({
|
|
307
368
|
name,
|
|
308
369
|
description = '',
|
|
309
370
|
time,
|
|
371
|
+
at,
|
|
310
372
|
timezone,
|
|
311
373
|
days,
|
|
312
374
|
channel,
|
|
@@ -316,78 +378,69 @@ export function saveSchedule({
|
|
|
316
378
|
overwrite = false,
|
|
317
379
|
} = {}) {
|
|
318
380
|
const id = assertName(name, 'schedule name');
|
|
319
|
-
const cron = normalizeCron(time);
|
|
320
381
|
const body = String(instructions || '').trim();
|
|
321
382
|
if (!body) throw new Error('schedule instructions are required');
|
|
322
383
|
if (channel && !model) throw new Error('model is required when channel is set');
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
384
|
+
const hasTime = time != null && String(time).trim() !== '';
|
|
385
|
+
const hasAt = at != null && String(at).trim() !== '';
|
|
386
|
+
if (hasTime && hasAt) throw new Error('provide either `time` (recurring) or `at` (one-shot), not both');
|
|
387
|
+
if (!hasTime && !hasAt) throw new Error('either `time` (recurring cron) or `at` (one-shot datetime) is required');
|
|
388
|
+
if (overwrite !== true && (await dbGetSchedule(id))) {
|
|
389
|
+
throw new Error(`schedule "${id}" already exists`);
|
|
390
|
+
}
|
|
391
|
+
const whenCron = hasTime ? foldDaysIntoCron(normalizeCron(time), days) : null;
|
|
392
|
+
const whenAt = hasAt ? parseAtDatetime(at) : null;
|
|
393
|
+
const saved = await upsertSchedule({
|
|
394
|
+
name: id,
|
|
395
|
+
description: String(description || '').trim(),
|
|
396
|
+
whenCron,
|
|
397
|
+
whenAt,
|
|
398
|
+
timezone: timezone ? String(timezone).trim() : null,
|
|
399
|
+
target: channel ? 'channel' : 'session',
|
|
400
|
+
channelId: channel ? String(channel).trim() : null,
|
|
401
|
+
model: model ? String(model).trim() : null,
|
|
402
|
+
prompt: body,
|
|
403
|
+
enabled: enabled !== false,
|
|
404
|
+
});
|
|
405
|
+
return scheduleToDisplay(saved);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export async function deleteSchedule(name) {
|
|
339
409
|
const id = assertName(name, 'schedule name');
|
|
340
|
-
|
|
410
|
+
await dbDeleteSchedule(id);
|
|
341
411
|
return { name: id, deleted: true };
|
|
342
412
|
}
|
|
343
413
|
|
|
344
|
-
export function setScheduleEnabled(name, enabled) {
|
|
414
|
+
export async function setScheduleEnabled(name, enabled) {
|
|
345
415
|
const id = assertName(name, 'schedule name');
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
if (!existsSync(mdPath)) throw new Error(`schedule "${id}" does not exist`);
|
|
349
|
-
const { frontmatter, body } = readMarkdownDocument(readText(mdPath, ''));
|
|
350
|
-
// Re-spread with name/description first so the round-trip keeps SKILL.md
|
|
351
|
-
// key order; name stays the dir slug regardless of stale frontmatter.
|
|
352
|
-
const { name: _staleName, description = '', ...rest } = frontmatter;
|
|
353
|
-
writeTextAtomic(mdPath, serializeFrontmatterDoc(
|
|
354
|
-
{ name: id, description, ...rest, enabled: enabled !== false },
|
|
355
|
-
body,
|
|
356
|
-
));
|
|
416
|
+
const updated = await dbSetEnabled(id, enabled !== false);
|
|
417
|
+
if (!updated) throw new Error(`schedule "${id}" does not exist`);
|
|
357
418
|
return { name: id, enabled: enabled !== false };
|
|
358
419
|
}
|
|
359
420
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
description,
|
|
381
|
-
...config,
|
|
382
|
-
secretSet: hasSecret,
|
|
383
|
-
secret: undefined,
|
|
384
|
-
instructions: body,
|
|
385
|
-
route: config.channel ? `channel:${config.channel}` : 'session',
|
|
386
|
-
};
|
|
387
|
-
});
|
|
421
|
+
// Webhook endpoints are stored in the PG table `webhooks.endpoints`
|
|
422
|
+
// (webhooks-db.mjs) — the single source of truth. Legacy per-endpoint
|
|
423
|
+
// WEBHOOK.md + secret folders are imported once at boot and deleted by the
|
|
424
|
+
// store's migration hook.
|
|
425
|
+
export async function listWebhooks() {
|
|
426
|
+
const endpoints = await dbListEndpoints();
|
|
427
|
+
return endpoints.map((ep) => ({
|
|
428
|
+
name: ep.name,
|
|
429
|
+
description: ep.description || '',
|
|
430
|
+
parser: ep.parser || 'github',
|
|
431
|
+
...(ep.channelId ? { channel: ep.channelId } : {}),
|
|
432
|
+
...(ep.model ? { model: ep.model } : {}),
|
|
433
|
+
enabled: ep.enabled,
|
|
434
|
+
// The store never projects the plaintext secret through list paths; it
|
|
435
|
+
// exposes a presence flag (secretSet) instead.
|
|
436
|
+
secretSet: ep.secretSet === true,
|
|
437
|
+
secret: undefined,
|
|
438
|
+
instructions: ep.instructions,
|
|
439
|
+
route: ep.channelId ? `channel:${ep.channelId}` : 'session',
|
|
440
|
+
}));
|
|
388
441
|
}
|
|
389
442
|
|
|
390
|
-
export function saveWebhook({
|
|
443
|
+
export async function saveWebhook({
|
|
391
444
|
name,
|
|
392
445
|
description = '',
|
|
393
446
|
parser = 'github',
|
|
@@ -406,47 +459,46 @@ export function saveWebhook({
|
|
|
406
459
|
const body = String(instructions || '').trim();
|
|
407
460
|
if (!body) throw new Error('webhook instructions are required');
|
|
408
461
|
if (channel && !model) throw new Error('model is required when channel is set');
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
462
|
+
if (overwrite !== true && (await dbLoadEndpoint(id))) {
|
|
463
|
+
throw new Error(`webhook "${id}" already exists`);
|
|
464
|
+
}
|
|
412
465
|
const secretValue = String(secret || randomBytes(24).toString('hex')).trim();
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
466
|
+
const saved = await dbUpsertEndpoint({
|
|
467
|
+
name: id,
|
|
468
|
+
description: String(description || '').trim(),
|
|
469
|
+
parser: nextParser,
|
|
470
|
+
channelId: channel ? String(channel).trim() : null,
|
|
471
|
+
model: model ? String(model).trim() : null,
|
|
472
|
+
secret: secretValue,
|
|
473
|
+
instructions: body,
|
|
474
|
+
enabled: enabled !== false,
|
|
475
|
+
});
|
|
476
|
+
return {
|
|
477
|
+
name: id,
|
|
478
|
+
description: saved.description,
|
|
479
|
+
parser: saved.parser,
|
|
480
|
+
...(saved.channelId ? { channel: saved.channelId } : {}),
|
|
481
|
+
...(saved.model ? { model: saved.model } : {}),
|
|
482
|
+
...(enabled === false ? { enabled: false } : {}),
|
|
483
|
+
secret: secretValue,
|
|
484
|
+
instructions: body,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export async function deleteWebhook(name) {
|
|
428
489
|
const id = assertName(name, 'webhook name');
|
|
429
|
-
|
|
490
|
+
await dbDeleteEndpoint(id);
|
|
430
491
|
return { name: id, deleted: true };
|
|
431
492
|
}
|
|
432
493
|
|
|
433
|
-
export function setWebhookEnabled(name, enabled) {
|
|
494
|
+
export async function setWebhookEnabled(name, enabled) {
|
|
434
495
|
const id = assertName(name, 'webhook name');
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
if (!existsSync(mdPath)) throw new Error(`webhook "${id}" does not exist`);
|
|
438
|
-
const { frontmatter, body } = readMarkdownDocument(readText(mdPath, ''));
|
|
439
|
-
// Re-spread with name/description first so the round-trip keeps SKILL.md
|
|
440
|
-
// key order; name stays the dir slug regardless of stale frontmatter.
|
|
441
|
-
const { name: _staleName, description = '', ...rest } = frontmatter;
|
|
442
|
-
writeTextAtomic(mdPath, serializeFrontmatterDoc(
|
|
443
|
-
{ name: id, description, ...rest, enabled: enabled !== false },
|
|
444
|
-
body,
|
|
445
|
-
));
|
|
496
|
+
const updated = await dbSetEndpointEnabled(id, enabled !== false);
|
|
497
|
+
if (!updated) throw new Error(`webhook "${id}" does not exist`);
|
|
446
498
|
return { name: id, enabled: enabled !== false };
|
|
447
499
|
}
|
|
448
500
|
|
|
449
|
-
export function channelSetup(config = null) {
|
|
501
|
+
export async function channelSetup(config = null) {
|
|
450
502
|
const cfg = normalizeChannelsConfig(config || readSection('channels'));
|
|
451
503
|
const discordToken = getDiscordToken();
|
|
452
504
|
const discordProblem = diagnoseDiscordTokenValue(discordToken, cfg);
|
|
@@ -475,13 +527,13 @@ export function channelSetup(config = null) {
|
|
|
475
527
|
status: webhookAuth ? 'Set' : 'Off',
|
|
476
528
|
},
|
|
477
529
|
channel: getChannel(cfg),
|
|
478
|
-
schedules: listSchedules(),
|
|
479
|
-
webhooks: listWebhooks(),
|
|
530
|
+
schedules: await listSchedules(),
|
|
531
|
+
webhooks: await listWebhooks(),
|
|
480
532
|
};
|
|
481
533
|
}
|
|
482
534
|
|
|
483
|
-
export function renderChannelStatus(config = null) {
|
|
484
|
-
const setup = channelSetup(config);
|
|
535
|
+
export async function renderChannelStatus(config = null) {
|
|
536
|
+
const setup = await channelSetup(config);
|
|
485
537
|
const lines = [];
|
|
486
538
|
lines.push(`discord ${setup.discord.status}${setup.discord.problem ? ` (${setup.discord.problem})` : ''}`);
|
|
487
539
|
lines.push(`webhook ${setup.webhook.enabled === false ? 'disabled' : 'enabled'} · auth ${setup.webhook.status} · port ${setup.webhook.port || 3333}`);
|
|
@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import { startChildGuardian } from '../runtime/shared/child-guardian.mjs';
|
|
7
|
+
import { detachedSpawnOpts } from '../runtime/shared/spawn-flags.mjs';
|
|
7
8
|
import { appendBuffered } from '../runtime/shared/buffered-appender.mjs';
|
|
8
9
|
import { scrubLoaderVars } from '../runtime/agent/orchestrator/tools/env-scrub.mjs';
|
|
9
10
|
import { rotateBoundedLog, PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_KEEP_BYTES } from '../lib/mixdog-debug.cjs';
|
|
@@ -441,9 +442,8 @@ export function createStandaloneChannelWorker({
|
|
|
441
442
|
cwd,
|
|
442
443
|
execArgv: ['--require', WORKER_PRELOAD],
|
|
443
444
|
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
|
444
|
-
detached: true,
|
|
445
445
|
env: daemonEnv(),
|
|
446
|
-
|
|
446
|
+
...detachedSpawnOpts,
|
|
447
447
|
});
|
|
448
448
|
} catch (err) {
|
|
449
449
|
logLine(logPath, `daemon spawn failed: ${err?.message || err}`);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { fork } from 'node:child_process';
|
|
2
|
+
import { detachedSpawnOpts } from '../runtime/shared/spawn-flags.mjs';
|
|
2
3
|
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
4
|
import http from 'node:http';
|
|
4
5
|
import { dirname, join, resolve } from 'node:path';
|
|
5
6
|
import { tmpdir } from 'node:os';
|
|
6
7
|
import { pathToFileURL } from 'node:url';
|
|
7
8
|
import { claimSingletonOwner, handoffSingletonOwner, readSingletonOwner, releaseSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
|
|
9
|
+
import { readLiveServiceAdvert } from '../runtime/shared/service-discovery.mjs';
|
|
8
10
|
import { scrubLoaderVars } from '../runtime/agent/orchestrator/tools/env-scrub.mjs';
|
|
9
11
|
import { rotateBoundedLog, PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_KEEP_BYTES } from '../lib/mixdog-debug.cjs';
|
|
10
12
|
|
|
@@ -253,10 +255,14 @@ export function createStandaloneMemoryRuntime({
|
|
|
253
255
|
}
|
|
254
256
|
|
|
255
257
|
async function findLivePort({ allowStarting = false } = {}) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
+
// Prefer the single-writer discovery advert (discovery/memory.json); the
|
|
259
|
+
// legacy active-instance.json memory_port/memory_server_pid fields remain a
|
|
260
|
+
// cross-version fallback when no discovery advert is present.
|
|
261
|
+
const advert = readLiveServiceAdvert('memory', { requirePid: false });
|
|
262
|
+
const active = advert ? null : readActiveInstance();
|
|
263
|
+
const port = advert ? parsePort(advert.port) : parsePort(active?.memory_port);
|
|
258
264
|
if (!port) return null;
|
|
259
|
-
const ownerPid = parsePid(active?.memory_server_pid);
|
|
265
|
+
const ownerPid = advert ? parsePid(advert.pid) : parsePid(active?.memory_server_pid);
|
|
260
266
|
// A dead server pid means the published memory_port is stale — the daemon
|
|
261
267
|
// that owned it is gone. Clearing portCache here (and letting the caller
|
|
262
268
|
// re-claim + respawn) prevents the stale port from wedging recovery.
|
|
@@ -384,7 +390,6 @@ export function createStandaloneMemoryRuntime({
|
|
|
384
390
|
cwd,
|
|
385
391
|
execArgv: [],
|
|
386
392
|
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
|
387
|
-
detached: true,
|
|
388
393
|
env: {
|
|
389
394
|
...daemonEnv,
|
|
390
395
|
MIXDOG_DATA_DIR: dataDir,
|
|
@@ -407,7 +412,7 @@ export function createStandaloneMemoryRuntime({
|
|
|
407
412
|
MIXDOG_MEMORY_DAEMON: '1',
|
|
408
413
|
MIXDOG_MEMORY_IDLE_TTL_MS: String(idleTtlMs),
|
|
409
414
|
},
|
|
410
|
-
|
|
415
|
+
...detachedSpawnOpts,
|
|
411
416
|
});
|
|
412
417
|
const childPid = child.pid;
|
|
413
418
|
if (singletonEnabled && childPid) {
|
package/src/tui/App.jsx
CHANGED
|
@@ -1338,7 +1338,14 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1338
1338
|
copySelection();
|
|
1339
1339
|
return;
|
|
1340
1340
|
}
|
|
1341
|
-
|
|
1341
|
+
// No app-owned selection. On Windows Terminal the same Ctrl+C is also the
|
|
1342
|
+
// native terminal's copy shortcut for a mouse selection we can't see — so
|
|
1343
|
+
// rendering a hint here fights that copy and flashes a spurious message.
|
|
1344
|
+
// Suppress the hint on win32 (interrupt routing is unchanged: Esc still
|
|
1345
|
+
// interrupts). Other platforms keep the guidance.
|
|
1346
|
+
if (process.platform !== 'win32') {
|
|
1347
|
+
showSelectionCopyHint('select text to copy · Esc interrupts', 'plain');
|
|
1348
|
+
}
|
|
1342
1349
|
return;
|
|
1343
1350
|
}
|
|
1344
1351
|
if (key.ctrl && (input === 'o' || input === 'O')) {
|
|
@@ -1855,19 +1862,34 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1855
1862
|
}
|
|
1856
1863
|
if (channelPrompt.kind === 'schedule-add') {
|
|
1857
1864
|
const [name, time, instructions, channel, model] = parts;
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1865
|
+
// saveSchedule is async (PG-backed). Only close the prompt / open the
|
|
1866
|
+
// list on success; surface a failure notice and keep the prompt open
|
|
1867
|
+
// so the input is not lost and the user can retry.
|
|
1868
|
+
Promise.resolve(store.saveSchedule({ name, time, instructions, channel, model }))
|
|
1869
|
+
.then(() => {
|
|
1870
|
+
setChannelPrompt(null);
|
|
1871
|
+
void openChannelSetupPicker('schedules');
|
|
1872
|
+
})
|
|
1873
|
+
.catch((e) => {
|
|
1874
|
+
store.pushNotice(`schedule save failed: ${e?.message || e}`, 'error');
|
|
1875
|
+
});
|
|
1861
1876
|
return true;
|
|
1862
1877
|
}
|
|
1863
1878
|
if (channelPrompt.kind === 'webhook-add') {
|
|
1864
1879
|
const [name, instructions, channel, model, parser] = parts;
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1880
|
+
// saveWebhook is async (PG-backed). Only close the prompt / open the
|
|
1881
|
+
// list on success; surface a failure notice and keep the prompt open.
|
|
1882
|
+
Promise.resolve(store.saveWebhook({ name, instructions, channel, model, parser }))
|
|
1883
|
+
.then((result) => {
|
|
1884
|
+
if (result?.secret) {
|
|
1885
|
+
store.pushNotice(`webhook secret for ${result.name}: ${result.secret}`, 'info');
|
|
1886
|
+
}
|
|
1887
|
+
setChannelPrompt(null);
|
|
1888
|
+
void openChannelSetupPicker('webhooks');
|
|
1889
|
+
})
|
|
1890
|
+
.catch((e) => {
|
|
1891
|
+
store.pushNotice(`webhook save failed: ${e?.message || e}`, 'error');
|
|
1892
|
+
});
|
|
1871
1893
|
return true;
|
|
1872
1894
|
}
|
|
1873
1895
|
} catch (e) {
|
|
@@ -27,7 +27,7 @@ export function createChannelPickers({
|
|
|
27
27
|
// a long first-time install finishing later must not hijack whatever
|
|
28
28
|
// screen/picker the user has since navigated to.
|
|
29
29
|
let pickerGen = 0;
|
|
30
|
-
const openChannelTypeActionsPicker = (backend, options = {}) => {
|
|
30
|
+
const openChannelTypeActionsPicker = async (backend, options = {}) => {
|
|
31
31
|
pickerGen += 1;
|
|
32
32
|
const parentReturn = typeof options.returnTo === 'function'
|
|
33
33
|
? options.returnTo
|
|
@@ -38,7 +38,7 @@ export function createChannelPickers({
|
|
|
38
38
|
setContextPanel(null);
|
|
39
39
|
let setup;
|
|
40
40
|
try {
|
|
41
|
-
setup = store.getChannelSetup();
|
|
41
|
+
setup = await store.getChannelSetup();
|
|
42
42
|
} catch (e) {
|
|
43
43
|
store.pushNotice(`channels failed: ${e?.message || e}`, 'error');
|
|
44
44
|
return;
|
|
@@ -125,7 +125,7 @@ export function createChannelPickers({
|
|
|
125
125
|
});
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
-
const openChannelSettingTypePicker = (options = {}) => {
|
|
128
|
+
const openChannelSettingTypePicker = async (options = {}) => {
|
|
129
129
|
pickerGen += 1;
|
|
130
130
|
const returnTo = typeof options.returnTo === 'function' ? options.returnTo : () => {};
|
|
131
131
|
setProviderPrompt(null);
|
|
@@ -135,7 +135,7 @@ export function createChannelPickers({
|
|
|
135
135
|
setContextPanel(null);
|
|
136
136
|
let setup;
|
|
137
137
|
try {
|
|
138
|
-
setup = store.getChannelSetup();
|
|
138
|
+
setup = await store.getChannelSetup();
|
|
139
139
|
} catch (e) {
|
|
140
140
|
store.pushNotice(`channels failed: ${e?.message || e}`, 'error');
|
|
141
141
|
return;
|
|
@@ -203,7 +203,7 @@ export function createChannelPickers({
|
|
|
203
203
|
setContextPanel(null);
|
|
204
204
|
let setup;
|
|
205
205
|
try {
|
|
206
|
-
setup = store.getChannelSetup();
|
|
206
|
+
setup = await store.getChannelSetup();
|
|
207
207
|
} catch (e) {
|
|
208
208
|
store.pushNotice(`channels failed: ${e?.message || e}`, 'error');
|
|
209
209
|
return;
|
|
@@ -239,14 +239,14 @@ export function createChannelPickers({
|
|
|
239
239
|
_action: 'noop',
|
|
240
240
|
}]),
|
|
241
241
|
];
|
|
242
|
-
const toggleSchedule = (item) => {
|
|
242
|
+
const toggleSchedule = async (item) => {
|
|
243
243
|
if (item._action !== 'schedule-toggle') return;
|
|
244
244
|
if (!channelRemoteEnabled) {
|
|
245
245
|
store.pushNotice('enable channel first', 'warn');
|
|
246
246
|
return;
|
|
247
247
|
}
|
|
248
248
|
try {
|
|
249
|
-
store.setScheduleEnabled?.(item._name, !item._enabled);
|
|
249
|
+
await store.setScheduleEnabled?.(item._name, !item._enabled);
|
|
250
250
|
void openChannelSetupPicker('schedules', { highlightValue: `schedule:${item._name}` });
|
|
251
251
|
} catch (e) {
|
|
252
252
|
store.pushNotice(`schedule toggle failed: ${e?.message || e}`, 'error');
|
|
@@ -346,14 +346,14 @@ export function createChannelPickers({
|
|
|
346
346
|
_action: 'noop',
|
|
347
347
|
}]),
|
|
348
348
|
];
|
|
349
|
-
const toggleWebhook = (item) => {
|
|
349
|
+
const toggleWebhook = async (item) => {
|
|
350
350
|
if (item._action !== 'webhook-toggle') return;
|
|
351
351
|
if (!channelRemoteEnabled) {
|
|
352
352
|
store.pushNotice('enable channel first', 'warn');
|
|
353
353
|
return;
|
|
354
354
|
}
|
|
355
355
|
try {
|
|
356
|
-
store.setWebhookEnabled?.(item._name, !item._enabled);
|
|
356
|
+
await store.setWebhookEnabled?.(item._name, !item._enabled);
|
|
357
357
|
void openChannelSetupPicker('webhooks', { highlightValue: `webhook:${item._name}` });
|
|
358
358
|
} catch (e) {
|
|
359
359
|
store.pushNotice(`webhook toggle failed: ${e?.message || e}`, 'error');
|
|
@@ -18,8 +18,22 @@ export function osc52ClipboardSequence(text) {
|
|
|
18
18
|
// helper. ~256KB of clipboard text → ~350KB of base64.
|
|
19
19
|
const OSC52_MAX_BYTES = 256 * 1024;
|
|
20
20
|
|
|
21
|
+
// On native Windows (ConPTY) a large OSC 52 base64 payload written synchronously
|
|
22
|
+
// to stdout can stall for multiple seconds under terminal backpressure while a
|
|
23
|
+
// mouse selection is active — a debugger-confirmed Ctrl+C copy freeze. clip.exe
|
|
24
|
+
// alone is the reliable local clipboard writer, so skip OSC 52 entirely on
|
|
25
|
+
// win32 UNLESS we're in a remote/multiplexed session (SSH or tmux) where OSC 52
|
|
26
|
+
// is the only way to reach the user's real terminal.
|
|
27
|
+
function shouldSkipOsc52() {
|
|
28
|
+
if (process.platform !== 'win32') return false;
|
|
29
|
+
if (process.env.TMUX) return false;
|
|
30
|
+
if (process.env.SSH_CONNECTION || process.env.SSH_TTY) return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
export function writeOsc52Clipboard(text) {
|
|
22
35
|
const value = String(text ?? '');
|
|
36
|
+
if (shouldSkipOsc52()) return false;
|
|
23
37
|
if (Buffer.byteLength(value, 'utf8') > OSC52_MAX_BYTES) return false;
|
|
24
38
|
try {
|
|
25
39
|
process.stdout.write(osc52ClipboardSequence(value));
|
package/src/tui/app/doctor.mjs
CHANGED
|
@@ -161,7 +161,7 @@ export async function buildDoctorReport(runtime = {}, getState = () => ({})) {
|
|
|
161
161
|
return;
|
|
162
162
|
}
|
|
163
163
|
const worker = settings.status || runtime.getChannelWorkerStatus?.() || {};
|
|
164
|
-
const setup = runtime.getChannelSetup?.() || {};
|
|
164
|
+
const setup = (await runtime.getChannelSetup?.()) || {};
|
|
165
165
|
const tokens = [];
|
|
166
166
|
if (setup.discord?.authenticated) tokens.push('discord');
|
|
167
167
|
if (setup.telegram?.authenticated) tokens.push('telegram');
|