mixdog 0.9.28 → 0.9.29

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.28",
3
+ "version": "0.9.29",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -5,5 +5,4 @@
5
5
  directly.
6
6
  - Use the session's current project/workspace. Change the work project only
7
7
  when the user asks for another project or a tool call needs another root.
8
- - Use `agent` for scoped implementation, research, review, and debugging — not
9
- for git commit/push/stash or Ship.
8
+ - Use `agent` for scoped implementation, research, review, and debugging.
@@ -1,10 +1,12 @@
1
1
  # Tool Use
2
2
 
3
- - Every turn carries ALL independent calls you can already specify a
4
- second consecutive single-lookup turn is a defect, not a style choice;
5
- serialize only on real data dependency. Merge variants/scopes into ONE
6
- call wherever the schema accepts arrays (`pattern[]`, `path[]`,
7
- `symbols[]`, `query[]`).
3
+ - BATCH OR IT IS A DEFECT this is the single hardest rule here. Every turn
4
+ fires ALL independent calls at once; a second consecutive single-lookup,
5
+ single-`shell`, or single-`apply_patch` turn is a defect, never a style
6
+ choice. Serialize ONLY on a real data dependency — nothing else earns a
7
+ solo call. Merge variants/scopes into ONE call wherever the schema takes
8
+ arrays (`pattern[]`, `path[]`, `symbols[]`, `query[]`), chain shell with
9
+ `;`/`&&` or run them in parallel, and put every known edit in ONE patch.
8
10
  - Route by what is already known: known symbol/relation → `code_graph`;
9
11
  exact text in a known scope → `grep`; unknown location, machine-wide/
10
12
  out-of-repo whereabouts, or concept-level question → `explore` (which uses
@@ -33,9 +35,9 @@
33
35
  unit (function/section) — over-read once instead of paging the same file
34
36
  in small windows across turns; a 3rd window into one file means the first
35
37
  should have been wider.
36
- - Don't mix `apply_patch` with shell or other state-changing calls in one
37
- turn; batch independent-file patches in one turn, then verify them all in
38
- ONE shell call the next.
39
- - Consecutive single `shell` or `apply_patch` turns are a batching miss
40
- unless output-dependent: chain shell commands with `;`/`&&` or call in
41
- parallel; put all known edits in ONE patch.
38
+ - Parallel-first has ONE carve-out: verifying your own edits. Keep an
39
+ `apply_patch` and the `shell` that checks it in separate turns not for
40
+ ordering (same-turn calls run in emit order), but because you must SEE the
41
+ patch result before verifying: a same-turn shell is already emitted and
42
+ can't react to a failed or partial patch. Batch the patches this turn,
43
+ verify them all in ONE shell call the next. Anything else stays parallel.
@@ -11,9 +11,7 @@ function defaultAccess() {
11
11
  function normalizeAccess(parsed) {
12
12
  const defaults = defaultAccess();
13
13
  return {
14
- // Legacy "pairing" policy was removed (its approval flow was never
15
- // completable); normalize it to "allowlist" on load.
16
- dmPolicy: parsed?.dmPolicy === "pairing" ? "allowlist" : (parsed?.dmPolicy ?? defaults.dmPolicy),
14
+ dmPolicy: parsed?.dmPolicy ?? defaults.dmPolicy,
17
15
  allowFrom: parsed?.allowFrom ?? defaults.allowFrom,
18
16
  channels: parsed?.channels ?? defaults.channels,
19
17
  mentionPatterns: parsed?.mentionPatterns,
@@ -56,7 +56,7 @@ function defaultAccess() {
56
56
  function normalizeAccess(parsed) {
57
57
  const defaults = defaultAccess();
58
58
  return {
59
- dmPolicy: parsed?.dmPolicy === "pairing" ? "allowlist" : (parsed?.dmPolicy ?? defaults.dmPolicy),
59
+ dmPolicy: parsed?.dmPolicy ?? defaults.dmPolicy,
60
60
  allowFrom: parsed?.allowFrom ?? defaults.allowFrom,
61
61
  channels: parsed?.channels ?? defaults.channels,
62
62
  mentionPatterns: parsed?.mentionPatterns,
@@ -39,27 +39,10 @@ function channelIdForBackend(entry = {}, backend = "discord") {
39
39
  }
40
40
 
41
41
  // Resolve the single active-backend channel id from the config's `channel`
42
- // section. Backward compat (read-side only, no on-disk migration): when
43
- // `channel` is absent, fall back to the legacy `channelsConfig[mainChannel]`
44
- // entry (or the first entry that carries an id).
42
+ // section. Disk config carries a single `channel` object only.
45
43
  function resolveChannelId(raw = {}, backend = "discord") {
46
44
  const channel = raw.channel && typeof raw.channel === "object" ? raw.channel : null;
47
45
  if (channel) return channelIdForBackend(channel, backend);
48
- const legacy = raw.channelsConfig && typeof raw.channelsConfig === "object" ? raw.channelsConfig : null;
49
- if (legacy) {
50
- const mainName = raw.mainChannel ?? "main";
51
- const preferred = legacy[mainName];
52
- if (preferred && typeof preferred === "object") {
53
- const id = channelIdForBackend(preferred, backend);
54
- if (id) return id;
55
- }
56
- for (const entry of Object.values(legacy)) {
57
- if (entry && typeof entry === "object") {
58
- const id = channelIdForBackend(entry, backend);
59
- if (id) return id;
60
- }
61
- }
62
- }
63
46
  return "";
64
47
  }
65
48
 
@@ -96,13 +79,11 @@ async function loadConfig() {
96
79
  const backend = raw.backend === "telegram" ? "telegram" : "discord";
97
80
  const telegramToken = getTelegramToken();
98
81
  const channelId = resolveChannelId(raw, backend);
99
- // Drop legacy multi-channel keys from the returned shape; the runtime
100
- // reads only the resolved `channelId` now (read-side compat lives in
101
- // resolveChannelId — no on-disk migration).
102
- const { channelsConfig: _legacyChannels, mainChannel: _legacyMain, ...rawRest } = raw;
82
+ // The runtime reads only the resolved `channelId`; disk carries a single
83
+ // `channel` object, so spread `raw` directly.
103
84
  return applyDefaults({
104
85
  ...DEFAULT_CONFIG,
105
- ...rawRest,
86
+ ...raw,
106
87
  backend,
107
88
  channelId,
108
89
  discord: { ...DEFAULT_CONFIG.discord, ...(({ token: _, ...rest }) => rest)(raw.discord || {}), ...(discordToken && !discordTokenProblem ? { token: discordToken } : {}) },
@@ -111,12 +92,7 @@ async function loadConfig() {
111
92
  telegram: { ...DEFAULT_CONFIG.telegram, ...(({ token: _t, ...rest }) => rest)(raw.telegram || {}), ...(telegramToken ? { token: telegramToken } : {}) },
112
93
  access: {
113
94
  ...DEFAULT_ACCESS,
114
- // Drop the retired pairing-era keys at the config layer too (the
115
- // Discord backend's normalizeAccess() is the runtime belt): legacy
116
- // dmPolicy "pairing" → "allowlist", and the `pending` code store is
117
- // gone entirely.
118
- ...(({ pending: _pending, ...rest }) => rest)(raw.access || {}),
119
- ...(raw.access?.dmPolicy === "pairing" ? { dmPolicy: "allowlist" } : {}),
95
+ ...(raw.access || {}),
120
96
  channels: accessChannels,
121
97
  },
122
98
  voice: { ...(raw.voice || {}), ...voice }
@@ -315,14 +315,9 @@ export function diagnoseDiscordTokenValue(value, config = {}) {
315
315
  const discord = config?.discord && typeof config.discord === 'object' ? config.discord : {}
316
316
  const appId = String(discord.applicationId || '').trim()
317
317
  if (appId && token === appId) return 'Bot token field contains the Application ID, not the bot token.'
318
- // Single main channel: check the `channel` object. Legacy `channelsConfig`
319
- // is still read here (read-only) so the diagnostic keeps working on configs
320
- // that predate the single-channel collapse.
318
+ // Single main channel: check the `channel` object.
321
319
  const candidateEntries = []
322
320
  if (config?.channel && typeof config.channel === 'object') candidateEntries.push(config.channel)
323
- if (config?.channelsConfig && typeof config.channelsConfig === 'object') {
324
- candidateEntries.push(...Object.values(config.channelsConfig))
325
- }
326
321
  for (const ch of candidateEntries) {
327
322
  if (!ch || typeof ch !== 'object') continue
328
323
  for (const id of [ch.channelId, ch.discordChannelId, ch.telegramChatId]) {
@@ -3,10 +3,8 @@
3
3
  * schedules (schema `scheduler`, table `scheduler.schedules`).
4
4
  *
5
5
  * All schedule readers/writers (scheduler.mjs, config.mjs, channel-admin.mjs)
6
- * go through this module. Legacy `<dataDir>/schedules/<name>/SCHEDULE.md`
7
- * files are imported once by the migration hook in getDb and the directory is
8
- * renamed to `schedules.migrated`; the old file-based schedules-store.mjs has
9
- * been removed.
6
+ * go through this module. It is the sole store for schedules; the old
7
+ * file-based schedules-store.mjs has been retired.
10
8
  *
11
9
  * DDL is idempotent and runs once on the first call per process. All queries
12
10
  * fully-qualify `scheduler.schedules` so they are correct regardless of the
@@ -15,9 +13,6 @@
15
13
 
16
14
  import { ensurePgInstance, withSchemaBootstrapLock } from '../memory/lib/pg/adapter.mjs';
17
15
  import { resolvePluginData } from './plugin-paths.mjs';
18
- import { readdirSync, readFileSync, renameSync, existsSync } from 'node:fs';
19
- import { join } from 'node:path';
20
- import { readMarkdownDocument } from './markdown-frontmatter.mjs';
21
16
 
22
17
  const SCHEMA = 'scheduler';
23
18
 
@@ -58,7 +53,6 @@ async function getDb(dataDir = resolvePluginData()) {
58
53
  // same cluster-global advisory lock the adapter uses for schema bootstrap,
59
54
  // so racing first calls can't run the DDL simultaneously.
60
55
  await withSchemaBootstrapLock(pool, () => db.exec(DDL));
61
- await migrateLegacySchedules(db, dataDir);
62
56
  return db;
63
57
  })();
64
58
  _ready.set(dataDir, p);
@@ -70,118 +64,6 @@ async function getDb(dataDir = resolvePluginData()) {
70
64
  }
71
65
  }
72
66
 
73
- // ---------------------------------------------------------------------------
74
- // One-time legacy SCHEDULE.md migration (additive; runs once per dataDir on
75
- // first getDb, right after DDL). Imports every `<dataDir>/schedules/<name>/
76
- // SCHEDULE.md` not already present in the table (by name), using the same
77
- // days->cron folding as the admin write path, then renames the directory to
78
- // `schedules.migrated` (never deletes user data). Per-entry failures are
79
- // isolated and never block store readiness.
80
- // ---------------------------------------------------------------------------
81
-
82
- // Day-name / keyword -> cron day-of-week number (Sun=0 .. Sat=6).
83
- const MIGRATE_DAY_TO_DOW = {
84
- sun: 0, sunday: 0,
85
- mon: 1, monday: 1,
86
- tue: 2, tues: 2, tuesday: 2,
87
- wed: 3, weds: 3, wednesday: 3,
88
- thu: 4, thur: 4, thurs: 4, thursday: 4,
89
- fri: 5, friday: 5,
90
- sat: 6, saturday: 6,
91
- };
92
-
93
- function foldLegacyDaysIntoCron(cron, days) {
94
- const parts = String(cron || '').trim().split(/\s+/).filter(Boolean);
95
- if (parts.length !== 5 && parts.length !== 6) {
96
- throw new Error(`invalid cron "${cron}"`);
97
- }
98
- const raw = String(days || '').trim().toLowerCase();
99
- const dowIndex = parts.length - 1;
100
- // days absent -> keep the cron's own day-of-week field ('0 9 * * 1' stays
101
- // Monday-only). Only an explicit selector rewrites the dow field.
102
- if (!raw) return parts.join(' ');
103
- let dow;
104
- if (raw === 'daily' || raw === 'everyday' || raw === 'every day') dow = '*';
105
- else if (raw === 'weekday' || raw === 'weekdays') dow = '1-5';
106
- else if (raw === 'weekend' || raw === 'weekends') dow = '0,6';
107
- else {
108
- const nums = raw.split(/[\s,]+/).filter(Boolean).map((t) => (
109
- /^[0-6]$/.test(t) ? Number(t) : MIGRATE_DAY_TO_DOW[t]
110
- ));
111
- if (nums.some((n) => n === undefined)) {
112
- throw new Error(`days "${days}" is not a recognizable day selector`);
113
- }
114
- dow = nums.join(',');
115
- }
116
- parts[dowIndex] = dow;
117
- return parts.join(' ');
118
- }
119
-
120
- async function migrateLegacySchedules(db, dataDir) {
121
- const dir = join(dataDir, 'schedules');
122
- let entries;
123
- try {
124
- entries = readdirSync(dir, { withFileTypes: true });
125
- } catch {
126
- return; // no legacy schedules/ dir -> nothing to migrate
127
- }
128
- let imported = 0;
129
- let skipped = 0;
130
- const failed = [];
131
- for (const ent of entries) {
132
- if (!ent.isDirectory()) continue;
133
- const name = ent.name;
134
- try {
135
- const { rows } = await db.query('SELECT 1 FROM scheduler.schedules WHERE name = $1', [name]);
136
- if (rows.length) { skipped++; continue; }
137
- let md;
138
- try { md = readFileSync(join(dir, name, 'SCHEDULE.md'), 'utf8'); }
139
- catch { skipped++; continue; }
140
- const { frontmatter, body } = readMarkdownDocument(md);
141
- const cron = foldLegacyDaysIntoCron(frontmatter.time, frontmatter.days);
142
- const channel = String(frontmatter.channel || '').trim();
143
- const enabled = frontmatter.enabled !== 'false' && frontmatter.enabled !== false;
144
- await db.query(
145
- `INSERT INTO scheduler.schedules
146
- (name, description, when_cron, timezone, target, channel_id, model, prompt, enabled)
147
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
148
- ON CONFLICT (name) DO NOTHING`,
149
- [
150
- name,
151
- String(frontmatter.description || '').trim(),
152
- cron,
153
- frontmatter.timezone ? String(frontmatter.timezone).trim() : null,
154
- channel ? 'channel' : 'session',
155
- channel || null,
156
- frontmatter.model ? String(frontmatter.model).trim() : null,
157
- String(body || '').trim(),
158
- enabled,
159
- ],
160
- );
161
- imported++;
162
- } catch (err) {
163
- failed.push(name);
164
- console.error(`[schedules] migration failed for "${name}": ${err?.message || err}`);
165
- }
166
- }
167
- if (failed.length) {
168
- // Some entries failed to import. Leave schedules/ in place (do NOT rename)
169
- // so the next boot retries them; already-imported entries are skipped by
170
- // the name check above, so retry is idempotent.
171
- console.error(`[schedules] migrated ${imported} legacy schedule(s), ${failed.length} failed (${failed.join(', ')}); leaving schedules/ for retry`);
172
- return;
173
- }
174
- try {
175
- let target = `${dir}.migrated`;
176
- if (existsSync(target)) target = `${dir}.migrated-${Date.now()}`;
177
- renameSync(dir, target);
178
- } catch (err) {
179
- console.error(`[schedules] migrated ${imported} legacy schedule(s) but could not rename schedules/: ${err?.message || err}`);
180
- return;
181
- }
182
- console.error(`[schedules] migrated ${imported} legacy schedule(s) (${skipped} skipped); renamed schedules/ -> schedules.migrated`);
183
- }
184
-
185
67
  // ---------------------------------------------------------------------------
186
68
  // Row <-> def mapping
187
69
  // ---------------------------------------------------------------------------
@@ -6,11 +6,9 @@
6
6
  * This module mirrors schedules-db.mjs: a lazy connection keyed per resolved
7
7
  * dataDir runs idempotent DDL exactly once per process, serialized across
8
8
  * concurrent first-boot processes on the adapter's schema-bootstrap advisory
9
- * lock. It covers the file-store API surface currently in
10
- * channels/lib/webhook/deliveries.mjs (loadEndpointConfig, appendDelivery,
11
- * deliveryExists, _readEndpointSecret) so call sites can migrate off the
12
- * per-endpoint WEBHOOK.md + deliveries.jsonl files later. No call-site
13
- * changes are made in this scope.
9
+ * lock. It is the sole store for endpoints + delivery dedup; the old
10
+ * per-endpoint WEBHOOK.md + deliveries.jsonl file store is fully retired
11
+ * (webhook.mjs reads/writes only through here).
14
12
  *
15
13
  * All queries fully-qualify their tables so they are correct regardless of
16
14
  * the connection search_path.
@@ -18,9 +16,6 @@
18
16
 
19
17
  import { ensurePgInstance, withSchemaBootstrapLock } from '../memory/lib/pg/adapter.mjs';
20
18
  import { resolvePluginData } from './plugin-paths.mjs';
21
- import { readdirSync, readFileSync, rmSync, rmdirSync } from 'node:fs';
22
- import { join } from 'node:path';
23
- import { readMarkdownDocument } from './markdown-frontmatter.mjs';
24
19
 
25
20
  const SCHEMA = 'webhooks';
26
21
 
@@ -67,7 +62,6 @@ async function getDb(dataDir = resolvePluginData()) {
67
62
  // same cluster-global advisory lock the adapter uses for schema bootstrap,
68
63
  // so racing first calls can't run the DDL simultaneously.
69
64
  await withSchemaBootstrapLock(pool, () => db.exec(DDL));
70
- await migrateLegacyWebhooks(db, dataDir);
71
65
  return db;
72
66
  })();
73
67
  _ready.set(dataDir, p);
@@ -79,91 +73,6 @@ async function getDb(dataDir = resolvePluginData()) {
79
73
  }
80
74
  }
81
75
 
82
- // ---------------------------------------------------------------------------
83
- // One-time legacy WEBHOOK.md migration (additive; runs once per dataDir on
84
- // first getDb, right after DDL). Imports every `<dataDir>/webhooks/<name>/
85
- // WEBHOOK.md` (+ its `secret` side file) not already present in the table (by
86
- // name), then DELETES the webhooks/ directory. The legacy file reading is
87
- // inlined here on purpose so this migration never depends on the file-store
88
- // deliveries.mjs module (which another scope may remove). Old per-endpoint
89
- // deliveries files are intentionally NOT imported — dedup history resets,
90
- // which is acceptable. Partial failure keeps the directory in place and logs,
91
- // so the next boot retries the un-imported entries idempotently.
92
- // ---------------------------------------------------------------------------
93
- async function migrateLegacyWebhooks(db, dataDir) {
94
- const dir = join(dataDir, 'webhooks');
95
- let entries;
96
- try {
97
- entries = readdirSync(dir, { withFileTypes: true });
98
- } catch {
99
- return; // no legacy webhooks/ dir -> nothing to migrate
100
- }
101
- let imported = 0;
102
- let skipped = 0;
103
- const failed = [];
104
- // Per-endpoint dirs safe to delete this run: those imported now, or already
105
- // present in the table (a prior run imported them). Anything unrecognized —
106
- // a dir with no WEBHOOK.md, a stray file, a failed import — is left in place.
107
- const removable = [];
108
- for (const ent of entries) {
109
- if (!ent.isDirectory()) continue;
110
- const name = ent.name;
111
- try {
112
- const { rows } = await db.query('SELECT 1 FROM webhooks.endpoints WHERE name = $1', [name]);
113
- if (rows.length) { skipped++; removable.push(name); continue; }
114
- let md;
115
- try { md = readFileSync(join(dir, name, 'WEBHOOK.md'), 'utf8'); }
116
- catch { skipped++; continue; }
117
- const { frontmatter, body } = readMarkdownDocument(md);
118
- let secret = null;
119
- try { secret = String(readFileSync(join(dir, name, 'secret'), 'utf8')).trim() || null; }
120
- catch { secret = null; }
121
- const channel = String(frontmatter.channel || '').trim();
122
- const enabled = frontmatter.enabled !== 'false' && frontmatter.enabled !== false;
123
- await db.query(
124
- `INSERT INTO webhooks.endpoints
125
- (name, description, channel_id, role, model, parser, secret, instructions, enabled)
126
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
127
- ON CONFLICT (name) DO NOTHING`,
128
- [
129
- name,
130
- String(frontmatter.description || '').trim(),
131
- channel || null,
132
- 'webhook-handler',
133
- frontmatter.model ? String(frontmatter.model).trim() : null,
134
- frontmatter.parser ? String(frontmatter.parser).trim() : null,
135
- secret,
136
- String(body || '').trim(),
137
- enabled,
138
- ],
139
- );
140
- imported++;
141
- removable.push(name);
142
- } catch (err) {
143
- failed.push(name);
144
- console.error(`[webhooks] migration failed for "${name}": ${err?.message || err}`);
145
- }
146
- }
147
- // User chose deletion over a `.migrated` rename. Delete only the recognized
148
- // per-endpoint dirs (imported now or already in the table); never blow away
149
- // unimported/unrelated entries. Failed imports keep their dir for the next
150
- // boot to retry (idempotent via the name check above).
151
- for (const name of removable) {
152
- try { rmSync(join(dir, name), { recursive: true, force: true }); }
153
- catch (err) { console.error(`[webhooks] could not delete migrated dir "${name}": ${err?.message || err}`); }
154
- }
155
- // Remove the parent webhooks/ only once it is empty — anything unrecognized
156
- // (stray files, WEBHOOK.md-less dirs, failed imports) keeps it alive.
157
- let parentGone = false;
158
- try {
159
- if (readdirSync(dir).length === 0) { rmdirSync(dir); parentGone = true; }
160
- } catch (err) {
161
- console.error(`[webhooks] could not remove empty webhooks/: ${err?.message || err}`);
162
- }
163
- const tail = failed.length ? `${failed.length} failed (${failed.join(', ')}); ` : '';
164
- console.error(`[webhooks] migrated ${imported} legacy webhook(s) (${skipped} skipped); ${tail}${parentGone ? 'removed webhooks/' : 'kept webhooks/ (non-empty)'}`);
165
- }
166
-
167
76
  // ---------------------------------------------------------------------------
168
77
  // Row <-> def mapping
169
78
  // ---------------------------------------------------------------------------
@@ -340,30 +249,6 @@ export async function claimDelivery(endpoint, deliveryId, fields = {}, { dataDir
340
249
  return { claimed: false, duplicate: true, row: rowToDelivery(existing[0]) };
341
250
  }
342
251
 
343
- /**
344
- * True when a delivery id has already been recorded for the endpoint (any
345
- * status). Callers wanting terminal-vs-inflight semantics inspect the row via
346
- * getDelivery; claimDelivery is the race-safe gate.
347
- */
348
- export async function deliveryExists(endpoint, deliveryId, { dataDir } = {}) {
349
- if (!endpoint || !deliveryId) return false;
350
- const db = await getDb(dataDir);
351
- const { rows } = await db.query(
352
- 'SELECT 1 FROM webhooks.deliveries WHERE endpoint = $1 AND delivery_id = $2',
353
- [endpoint, deliveryId],
354
- );
355
- return rows.length > 0;
356
- }
357
-
358
- export async function getDelivery(endpoint, deliveryId, { dataDir } = {}) {
359
- const db = await getDb(dataDir);
360
- const { rows } = await db.query(
361
- `SELECT ${DELIVERY_COLS} FROM webhooks.deliveries WHERE endpoint = $1 AND delivery_id = $2`,
362
- [endpoint, deliveryId],
363
- );
364
- return rowToDelivery(rows[0]);
365
- }
366
-
367
252
  /**
368
253
  * Update the status (and optional event/error) of an existing delivery,
369
254
  * keyed by (endpoint, delivery_id). Returns the updated row or null.
@@ -383,23 +268,3 @@ export async function updateDeliveryStatus(endpoint, deliveryId, status, fields
383
268
  );
384
269
  return rowToDelivery(rows[0]);
385
270
  }
386
-
387
- /**
388
- * Append a delivery record (claim-or-update convenience covering the old
389
- * appendDelivery). First write for an id claims it; a later write with the
390
- * same id updates its status/fields latest-wins.
391
- *
392
- * Returns the claim outcome — { claimed, duplicate, row } — NOT a bare row,
393
- * so a pre-dispatch caller can detect a concurrent duplicate (claimed:false,
394
- * duplicate:true) and skip re-dispatching an in-flight delivery. `row` is the
395
- * freshly claimed row when claimed, else the existing row after the
396
- * latest-wins status/fields update.
397
- */
398
- export async function appendDelivery(endpoint, entry = {}, { dataDir } = {}) {
399
- const deliveryId = entry.deliveryId ?? entry.id;
400
- if (!endpoint || !deliveryId) throw new Error('appendDelivery: endpoint and deliveryId are required');
401
- const claim = await claimDelivery(endpoint, deliveryId, entry, { dataDir });
402
- if (claim.claimed) return claim;
403
- const row = await updateDeliveryStatus(endpoint, deliveryId, entry.status ?? claim.row?.status, entry, { dataDir });
404
- return { claimed: false, duplicate: true, row };
405
- }
@@ -102,42 +102,22 @@ function seedBackendChannelIds(entry = {}, backend = 'discord') {
102
102
  return next;
103
103
  }
104
104
 
105
- // Resolve the single-channel entry from the config: prefer the new `channel`
106
- // object; fall back (read-side only, no on-disk migration) to the legacy
107
- // channelsConfig[mainChannel ?? 'main'] entry, then the first entry with an id.
105
+ // Resolve the single-channel entry from the config's `channel` object.
108
106
  function resolveChannelEntry(cfg = {}) {
109
107
  if (cfg.channel && typeof cfg.channel === 'object'
110
108
  && (cfg.channel.channelId || cfg.channel.discordChannelId || cfg.channel.telegramChatId)) {
111
109
  return cfg.channel;
112
110
  }
113
- const legacy = cfg.channelsConfig && typeof cfg.channelsConfig === 'object' ? cfg.channelsConfig : null;
114
- if (legacy) {
115
- const mainName = cfg.mainChannel ?? 'main';
116
- const preferred = legacy[mainName];
117
- if (preferred && typeof preferred === 'object'
118
- && (preferred.channelId || preferred.discordChannelId || preferred.telegramChatId)) {
119
- return preferred;
120
- }
121
- for (const entry of Object.values(legacy)) {
122
- if (entry && typeof entry === 'object'
123
- && (entry.channelId || entry.discordChannelId || entry.telegramChatId)) {
124
- return entry;
125
- }
126
- }
127
- }
128
111
  return cfg.channel && typeof cfg.channel === 'object' ? cfg.channel : {};
129
112
  }
130
113
 
131
114
  function updateChannelsSection(build) {
132
115
  let next;
133
116
  updateSection('channels', (current) => {
134
- // Preserve any legacy channelsConfig/mainChannel that already live on disk
135
- // for read-side compat, but never re-emit them from our writers: strip them
136
- // out of the returned shape so writes converge on the single `channel`.
117
+ // Writes converge on the single `channel` object.
137
118
  const normalized = normalizeChannelsConfig(current);
138
119
  next = build(normalized);
139
- const { channelsConfig: _lc, mainChannel: _lm, ...clean } = normalizeChannelsConfig(next);
140
- return clean;
120
+ return normalizeChannelsConfig(next);
141
121
  });
142
122
  return next;
143
123
  }
@@ -151,8 +131,7 @@ async function updateChannelsSectionAsync(build) {
151
131
  await updateSectionAsync('channels', (current) => {
152
132
  const normalized = normalizeChannelsConfig(current);
153
133
  next = build(normalized);
154
- const { channelsConfig: _lc, mainChannel: _lm, ...clean } = normalizeChannelsConfig(next);
155
- return clean;
134
+ return normalizeChannelsConfig(next);
156
135
  });
157
136
  return next;
158
137
  }
@@ -168,8 +147,8 @@ function listEntryDirs(dir) {
168
147
  }
169
148
  }
170
149
 
171
- // Single-channel read: resolves `cfg.channel` (legacy channelsConfig fallback)
172
- // into the flat shape the settings/TUI layer consumes.
150
+ // Single-channel read: resolves `cfg.channel` into the flat shape the
151
+ // settings/TUI layer consumes.
173
152
  export function getChannel(config = {}) {
174
153
  const cfg = normalizeChannelsConfig(config);
175
154
  const backend = cfg.backend === 'telegram' ? 'telegram' : 'discord';
@@ -38,9 +38,12 @@ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme
38
38
  * markers (agent card ←/→, history ↑/↓): WT draws them 1 cell in
39
39
  * Cascadia, and widening them ate the marker's gutter padding space
40
40
  * ("←Spawn" rendered glued / shifted vs the ● rows).
41
+ * Also excludes U+21BB (↻, the statusline quota-reset marker): WT/Cascadia
42
+ * draws it 1 cell, so widening it reserved a phantom cell that shifted the
43
+ * right-aligned workflow label one column left when the usage segment appeared.
41
44
  */
42
45
  export function isProblemCodePoint(cp) {
43
- return (cp >= 0x2460 && cp <= 0x24ff) || (cp >= 0x2194 && cp <= 0x21ff);
46
+ return (cp >= 0x2460 && cp <= 0x24ff) || (cp >= 0x2194 && cp <= 0x21ff && cp !== 0x21bb);
44
47
  }
45
48
 
46
49
  // Fast precheck for the problem ranges above. Lets the hot path bail before
@@ -4342,7 +4342,7 @@ import { Box as Box3, Text as Text3, useInput, usePaste, useStdin } from "../../
4342
4342
  import stringWidth from "string-width";
4343
4343
  var graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
4344
4344
  function isProblemCodePoint(cp) {
4345
- return cp >= 9312 && cp <= 9471 || cp >= 8596 && cp <= 8703;
4345
+ return cp >= 9312 && cp <= 9471 || cp >= 8596 && cp <= 8703 && cp !== 8635;
4346
4346
  }
4347
4347
  var PROBLEM_RE = /[\u2194-\u21ff\u2460-\u24ff]/;
4348
4348
  function resolveAmbiguousWidePolicy(env = process.env, platform = process.platform) {
@@ -23,14 +23,14 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity
23
23
  1. Plan — present a draft plan before ANY implementation; if not approved,
24
24
  revise and re-present (ping-pong) until an explicit go-ahead.
25
25
  2. Delegate — maximize distribution: split the work into as many independent
26
- scopes as possible and hand each to its own agent, all spawned in the SAME
27
- turn (parallel by default; sequential steps only inside one complex scope,
28
- gated build/test-green). This applies to every role alike — worker,
29
- heavy-worker, reviewer, debugger: fan them out across agents, never one at
30
- a time. Only a genuinely inseparable single scope stays whole, and say so.
31
- Briefs per the Lead brief contract. After spawning async agents, END THE
32
- TURN.
33
- 3. Review pair one reviewer 1:1 per implementation scope, same turn.
26
+ implementation scopes as possible and hand each to its own worker or
27
+ heavy-worker, all spawned in the SAME turn (parallel by default; sequential
28
+ steps only inside one complex scope, gated build/test-green). Fan them out
29
+ across agents, never one at a time. Only a genuinely inseparable single
30
+ scope stays whole, and say so. Briefs per the Lead brief contract. After
31
+ spawning async agents, END THE TURN.
32
+ 3. Review — spawn one reviewer 1:1 per implementation scope, all reviewers in
33
+ the same turn once their scopes land.
34
34
  Cross-check agent results yourself; send fixes back to the original scope
35
35
  and loop fix -> re-verify until clean. Skip only for simple low-risk work.
36
36
  Debugger first when the user asks for debugging or a bug survives 2+ fix
@@ -39,7 +39,7 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity
39
39
  and how work proceeds, marked in-progress — never as a conclusion.
40
40
  4. Report — final report briefs the whole work vs the approved plan and the
41
41
  verified result, distinct from interim updates. Never forward raw agent
42
- output. Ask about ship/deploy when relevant; deploy/build/commit only
43
- after user feedback with no issues.
42
+ output. Ask about ship/deploy when relevant; deploy/build/commit only on an
43
+ explicit user request, after feedback with no issues.
44
44
 
45
45
  On major direction shifts mid-work, pause and re-consult the user.
@@ -24,7 +24,9 @@ import stringWidth from 'string-width';
24
24
  const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
25
25
 
26
26
  function isProblemCodePoint(cp) {
27
- return (cp >= 0x2460 && cp <= 0x24ff) || (cp >= 0x2194 && cp <= 0x21ff);
27
+ // [mixdog fork] U+21BB (↻ quota-reset marker) excluded: WT draws it 1 cell,
28
+ // so widening it shifted the right-aligned statusline label one col left.
29
+ return (cp >= 0x2460 && cp <= 0x24ff) || (cp >= 0x2194 && cp <= 0x21ff && cp !== 0x21bb);
28
30
  }
29
31
 
30
32
  // [mixdog fork] Fast precheck for the problem ranges above. Lets the hot path