@moxxy/cli 0.13.0 → 0.13.2

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/dist/bin.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { z as z$1, createMutex, defineTunnelProvider, isCliTunnelAvailable, definePlugin, defineProvider, defineTool, MoxxyError, writeFileAtomic, asTurnId, defineMode, asPluginId, defineCommand, defineChannel, spawnCliTunnel, defineWorkflowExecutor, toFriendlyError, estimateTextTokens, classifyHttpStatus, createStuckLoopDetector, runCompactionIfNeeded, runElisionIfNeeded, collectProviderStream, usageEventFields, isContextOverflowError, emitRequestsAndDetectStuck, executeToolUses, buildSystemPromptWithSkills, projectMessages, defineCompactor, defineCacheStrategy, denyByDefaultResolver, createAllowListResolver, moxxyPath, moxxyHome, zodToJsonSchema, fileDiffSummary, runSingleShotTurn, bearerTokenMatches, resolveChannelToken, rotateChannelToken, defineSurface, estimateContextTokens as estimateContextTokens$1, readRequestBody, isFileDiffDisplay, MOXXY_WS_SUBPROTOCOL, renderFrontmatter, defineEmbedder, migrateModeName, bearerGuard, tokenFromWsProtocolHeader, skillFrontmatterSchema, asSkillId, getInstallHint, parseFrontmatterFile, createDeferredPermissionResolver, writeFileAtomicSync, encodeLoginPrompt, defineTranscriber, summarizeTokensByModel, countNodes, moxxyPackageSchema, classifyNetworkError, addModelTotals, createJsonFileStore, ISOLATION_RANK, fileDiffVerb, parseFrontmatter, createCallbackResolver, autoAllowResolver, asSessionId, asToolCallId, defineViewRenderer, DEFAULT_VIEW_TAGS, assertNever, isSafeViewUrl, evaluateToolRule, summarizeSessionTokensFromEvents, toDiffRows, diffGutterNo, computeElisionState, toolResultStubbed, toolResultStub, toolResultBytes, conversationalStubbed, conversationalStub, asEventId } from '@moxxy/sdk';
3
+ import { z as z$1, createMutex, defineTunnelProvider, definePlugin, defineProvider, defineTool, MoxxyError, asTurnId, defineMode, asPluginId, defineCommand, defineChannel, defineWorkflowExecutor, toFriendlyError, estimateTextTokens, classifyHttpStatus, createStuckLoopDetector, runCompactionIfNeeded, runElisionIfNeeded, collectProviderStream, usageEventFields, isContextOverflowError, emitRequestsAndDetectStuck, executeToolUses, buildSystemPromptWithSkills, projectMessages, defineCompactor, defineCacheStrategy, denyByDefaultResolver, createAllowListResolver, zodToJsonSchema, fileDiffSummary, runSingleShotTurn, defineSurface, runManualCompaction, isFileDiffDisplay, renderFrontmatter, defineEmbedder, migrateModeName, skillFrontmatterSchema, asSkillId, getInstallHint, parseFrontmatterFile, createDeferredPermissionResolver, encodeLoginPrompt, defineTranscriber, summarizeTokensByModel, countNodes, moxxyPackageSchema, classifyNetworkError, addModelTotals, createJsonFileStore, ISOLATION_RANK, MOXXY_PCM16_24KHZ_MIME, fileDiffVerb, parseFrontmatter, createCallbackResolver, autoAllowResolver, asSessionId, asToolCallId, defineViewRenderer, DEFAULT_VIEW_TAGS, assertNever, isSafeViewUrl, evaluateToolRule, summarizeSessionTokensFromEvents, toDiffRows, diffGutterNo, computeElisionState, toolResultStubbed, toolResultStub, toolResultBytes, conversationalStubbed, conversationalStub, asEventId } from '@moxxy/sdk';
4
4
  import * as fs32 from 'fs';
5
- import fs32__default, { existsSync, promises, ReadStream, mkdirSync, statSync, readdirSync, writeFileSync, readFileSync, chmodSync, unlinkSync, watch, createReadStream } from 'fs';
5
+ import fs32__default, { existsSync, promises, ReadStream, mkdirSync, statSync, readdirSync, writeFileSync, readFileSync, unlinkSync, chmodSync, watch, createReadStream } from 'fs';
6
6
  import * as path3 from 'path';
7
7
  import path3__default, { join, dirname, basename } from 'path';
8
+ import { isCliTunnelAvailable, writeFileAtomic, spawnCliTunnel, moxxyPath, moxxyHome, bearerTokenMatches, resolveChannelToken, rotateChannelToken, readRequestBody, MOXXY_WS_SUBPROTOCOL, bearerGuard, tokenFromWsProtocolHeader, writeFileAtomicSync } from '@moxxy/sdk/server';
8
9
  import { z } from 'zod';
9
10
  import * as os5 from 'os';
10
11
  import os5__default, { homedir, tmpdir, userInfo } from 'os';
@@ -14,7 +15,7 @@ import Stream, { Readable as Readable$1, PassThrough as PassThrough$2, Stream as
14
15
  import http, { createServer } from 'http';
15
16
  import https from 'https';
16
17
  import zlib from 'zlib';
17
- import { webcrypto, randomBytes, createHash, randomUUID, scryptSync, createCipheriv, createDecipheriv, createHmac, timingSafeEqual } from 'crypto';
18
+ import { randomUUID, webcrypto, randomBytes, createHash, scryptSync, createCipheriv, createDecipheriv, createHmac, timingSafeEqual } from 'crypto';
18
19
  import 'zod/v3';
19
20
  import * as z4mini from 'zod/v4-mini';
20
21
  import * as z53 from 'zod/v4';
@@ -223,6 +224,8 @@ var init_log = __esm({
223
224
  this.now = opts.now ?? Date.now;
224
225
  for (const e3 of seed)
225
226
  this.events.push(e3);
227
+ if (seed.length > 0)
228
+ this.base = seed[0].seq;
226
229
  }
227
230
  get length() {
228
231
  return this.events.length;
@@ -447,15 +450,20 @@ async function streamChildEventToParent(parentSession, parentTurnId, label3, chi
447
450
  const mapped = mapChildEvent(label3, childSessionId, childEvt);
448
451
  if (!mapped)
449
452
  return;
450
- await parentSession.log.append({
451
- type: "plugin_event",
452
- sessionId: parentSession.id,
453
- turnId: parentTurnId,
454
- source: "plugin",
455
- pluginId: SUBAGENT_PLUGIN_ID,
456
- subtype: mapped.subtype,
457
- payload: mapped.payload
458
- });
453
+ try {
454
+ await parentSession.log.append({
455
+ type: "plugin_event",
456
+ sessionId: parentSession.id,
457
+ turnId: parentTurnId,
458
+ source: "plugin",
459
+ pluginId: SUBAGENT_PLUGIN_ID,
460
+ subtype: mapped.subtype,
461
+ payload: mapped.payload
462
+ });
463
+ } catch (err) {
464
+ process.stderr.write(`moxxy: dropped subagent progress event (${mapped.subtype}) \u2014 parent log append failed: ${err instanceof Error ? err.message : String(err)}
465
+ `);
466
+ }
459
467
  }
460
468
  function mapChildEvent(label3, childSessionId, childEvt) {
461
469
  const payload = {
@@ -1561,7 +1569,16 @@ var init_providers = __esm({
1561
1569
  if (instance)
1562
1570
  this.instances.set(def.name, instance);
1563
1571
  }
1564
- /** Overwrite an existing def (also drops the cached instance so the new createClient gets called). */
1572
+ /**
1573
+ * Overwrite an existing def (also drops the cached instance so the new
1574
+ * createClient gets called).
1575
+ *
1576
+ * Invariant: replacing the ACTIVE provider's def leaves `active` pointing at
1577
+ * it but with no cached instance, so `getActive()` throws until the caller
1578
+ * rebuilds the instance. Callers replacing the active provider MUST follow
1579
+ * with `setActive(name, config)` (replace can't rebuild itself — it has no
1580
+ * config). The sole production caller does exactly that.
1581
+ */
1565
1582
  replace(def, instance) {
1566
1583
  this.defs.set(def.name, def);
1567
1584
  this.instances.delete(def.name);
@@ -1654,8 +1671,12 @@ var init_modes = __esm({
1654
1671
  }
1655
1672
  replace(mode) {
1656
1673
  this.modes.set(mode.name, mode);
1657
- if (!this.active)
1674
+ if (!this.active) {
1658
1675
  this.activate(mode);
1676
+ } else if (this.active === mode.name) {
1677
+ for (const fn of this.changeListeners)
1678
+ fn();
1679
+ }
1659
1680
  }
1660
1681
  /**
1661
1682
  * Remove a mode. If it was active, the active slot is cleared —
@@ -1994,16 +2015,17 @@ function coerceValue(tag2, name, value, spec, errors2) {
1994
2015
  }
1995
2016
  return num;
1996
2017
  }
2018
+ const decoded = decodeEntities(value);
1997
2019
  if (spec.type === "enum") {
1998
- if (!spec.values?.includes(value)) {
2020
+ if (!spec.values?.includes(decoded)) {
1999
2021
  errors2.push({ message: `<${tag2}> attribute "${name}" must be one of: ${spec.values?.join(", ")}` });
2000
2022
  }
2001
- return value;
2023
+ return decoded;
2002
2024
  }
2003
- if ((name === "href" || name === "src") && !isSafeViewUrl(value, name)) {
2025
+ if ((name === "href" || name === "src") && !isSafeViewUrl(decoded, name)) {
2004
2026
  errors2.push({ message: `<${tag2}> attribute "${name}" has a disallowed URL scheme` });
2005
2027
  }
2006
- return value;
2028
+ return decoded;
2007
2029
  }
2008
2030
  function coerceAttrs(b3, spec, errors2) {
2009
2031
  const out = {};
@@ -2015,7 +2037,8 @@ function coerceAttrs(b3, spec, errors2) {
2015
2037
  }
2016
2038
  const aspec = spec?.attrs[a2.name];
2017
2039
  if (!aspec) {
2018
- errors2.push({ message: `<${b3.tag}> unknown attribute "${a2.name}"` });
2040
+ if (spec)
2041
+ errors2.push({ message: `<${b3.tag}> unknown attribute "${a2.name}"` });
2019
2042
  continue;
2020
2043
  }
2021
2044
  seen.add(a2.name);
@@ -2229,10 +2252,13 @@ var init_localhost = __esm({
2229
2252
  "../core/dist/tunnel/localhost.js"() {
2230
2253
  localhostTunnel = defineTunnelProvider({
2231
2254
  name: "localhost",
2232
- open: (opts) => Promise.resolve({
2233
- url: `http://${opts.host}:${opts.port}`,
2234
- close: () => Promise.resolve()
2235
- }),
2255
+ open: (opts) => {
2256
+ const host = opts.host.includes(":") ? `[${opts.host}]` : opts.host;
2257
+ return Promise.resolve({
2258
+ url: `http://${host}:${opts.port}`,
2259
+ close: () => Promise.resolve()
2260
+ });
2261
+ },
2236
2262
  isAvailable: () => Promise.resolve(true)
2237
2263
  });
2238
2264
  }
@@ -3092,7 +3118,7 @@ function matchRule(rule, call, intent) {
3092
3118
  if (!input || typeof input !== "object")
3093
3119
  return false;
3094
3120
  for (const [k3, v3] of Object.entries(rule.inputMatches)) {
3095
- const candidate = String(input[k3] ?? "");
3121
+ const candidate = stringifyCandidate(input[k3]);
3096
3122
  let re2;
3097
3123
  try {
3098
3124
  re2 = new RegExp(v3);
@@ -3108,6 +3134,18 @@ function matchRule(rule, call, intent) {
3108
3134
  }
3109
3135
  return true;
3110
3136
  }
3137
+ function stringifyCandidate(value) {
3138
+ if (value === null || value === void 0)
3139
+ return "";
3140
+ if (typeof value === "object") {
3141
+ try {
3142
+ return JSON.stringify(value) ?? "";
3143
+ } catch {
3144
+ return String(value);
3145
+ }
3146
+ }
3147
+ return String(value);
3148
+ }
3111
3149
  function warnBadPattern(ruleName, field, pattern, intent, err) {
3112
3150
  const detail = err instanceof Error ? err.message : String(err);
3113
3151
  const resolution = intent === "deny" ? "failing closed (rule still denies)" : "this field cannot match (rule will not grant)";
@@ -3760,7 +3798,16 @@ async function loadDir(dir, scope, logger) {
3760
3798
  if (!entry.isFile() || !entry.name.endsWith(".md"))
3761
3799
  continue;
3762
3800
  const full = path3.join(dir, entry.name);
3763
- const raw = await promises.readFile(full, "utf8");
3801
+ let raw;
3802
+ try {
3803
+ raw = await promises.readFile(full, "utf8");
3804
+ } catch (err) {
3805
+ logger?.warn("skill: unreadable file, skipping", {
3806
+ path: full,
3807
+ error: err instanceof Error ? err.message : String(err)
3808
+ });
3809
+ continue;
3810
+ }
3764
3811
  const { frontmatter, body } = parseSkillFile(raw);
3765
3812
  const parsed = skillFrontmatterSchema.safeParse(frontmatter);
3766
3813
  if (!parsed.success) {
@@ -4211,6 +4258,17 @@ var init_persistence = __esm({
4211
4258
  this.indexUpdateScheduled = false;
4212
4259
  await this.writeIndex();
4213
4260
  }
4261
+ /**
4262
+ * Resolve once every event-log write queued so far has settled. Appends are
4263
+ * enqueued fire-and-forget (`enqueueAppend`), so callers that need to observe
4264
+ * the on-disk result of prior appends — graceful shutdown, or a test that
4265
+ * mutates the filesystem between writes — await this to drain the queue
4266
+ * rather than guessing at timing. Enqueues a no-op at the tail of the same
4267
+ * mutex, so it can only resolve after all earlier appends/truncates have run.
4268
+ */
4269
+ async settleWrites() {
4270
+ await this.writeQueue.run(() => void 0);
4271
+ }
4214
4272
  /**
4215
4273
  * Manually update header fields (provider/model) when the user
4216
4274
  * switches mid-session. The /model picker calls this so the index
@@ -4233,7 +4291,7 @@ var init_persistence = __esm({
4233
4291
  };
4234
4292
  this.scheduleIndexWrite();
4235
4293
  const line = JSON.stringify(event) + "\n";
4236
- void this.writeQueue.run(() => promises.appendFile(this.logPath, line, "utf8")).then(() => this.noteWriteOk()).catch((err) => this.noteWriteFailure("append", err));
4294
+ void this.writeQueue.run(() => this.ensureReady().then(() => promises.appendFile(this.logPath, line, "utf8"))).then(() => this.noteWriteOk()).catch((err) => this.noteWriteFailure("append", err));
4237
4295
  }
4238
4296
  /**
4239
4297
  * True while event-log writes are failing (history is no longer being
@@ -4274,7 +4332,7 @@ var init_persistence = __esm({
4274
4332
  lastActivity: (/* @__PURE__ */ new Date()).toISOString()
4275
4333
  };
4276
4334
  this.scheduleIndexWrite();
4277
- void this.writeQueue.run(() => promises.writeFile(this.logPath, "", "utf8")).then(() => this.noteWriteOk()).catch((err) => this.noteWriteFailure("truncate", err));
4335
+ void this.writeQueue.run(() => this.ensureReady().then(() => promises.writeFile(this.logPath, "", "utf8"))).then(() => this.noteWriteOk()).catch((err) => this.noteWriteFailure("truncate", err));
4278
4336
  }
4279
4337
  scheduleIndexWrite() {
4280
4338
  if (this.indexUpdateScheduled)
@@ -30607,7 +30665,7 @@ var require_api = __commonJS({
30607
30665
  Object.defineProperty(exports, "__esModule", { value: true });
30608
30666
  exports.Api = void 0;
30609
30667
  var client_js_1 = require_client();
30610
- var Api = class {
30668
+ var Api2 = class {
30611
30669
  /**
30612
30670
  * Constructs a new instance of `Api`. It is independent from all other
30613
30671
  * instances of this class. For example, this lets you install a custom set
@@ -32942,7 +33000,7 @@ var require_api = __commonJS({
32942
33000
  return this.raw.getGameHighScores({ inline_message_id, user_id }, signal);
32943
33001
  }
32944
33002
  };
32945
- exports.Api = Api;
33003
+ exports.Api = Api2;
32946
33004
  }
32947
33005
  });
32948
33006
 
@@ -32986,7 +33044,7 @@ var require_bot = __commonJS({
32986
33044
  "chat_boost",
32987
33045
  "removed_chat_boost"
32988
33046
  ];
32989
- var Bot3 = class extends composer_js_1.Composer {
33047
+ var Bot2 = class extends composer_js_1.Composer {
32990
33048
  /**
32991
33049
  * Creates a new Bot with the given token.
32992
33050
  *
@@ -33352,7 +33410,7 @@ var require_bot = __commonJS({
33352
33410
  await sleep7(sleepSeconds);
33353
33411
  }
33354
33412
  };
33355
- exports.Bot = Bot3;
33413
+ exports.Bot = Bot2;
33356
33414
  async function withRetries(task, signal) {
33357
33415
  const INITIAL_DELAY = 50;
33358
33416
  let lastDelay = INITIAL_DELAY;
@@ -49252,11 +49310,25 @@ function renderResult(content, isError) {
49252
49310
  else if (block.type === "image")
49253
49311
  parts.push(`[image:${block.mimeType}]`);
49254
49312
  else if (block.type === "resource")
49255
- parts.push(`[resource]`);
49313
+ parts.push(renderResource(block.resource));
49256
49314
  }
49257
49315
  const text = parts.join("\n");
49258
49316
  return isError ? `[error] ${text}` : text;
49259
49317
  }
49318
+ function renderResource(resource) {
49319
+ if (resource && typeof resource === "object") {
49320
+ const r2 = resource;
49321
+ if (typeof r2.text === "string")
49322
+ return r2.text;
49323
+ const meta = [
49324
+ typeof r2.uri === "string" ? r2.uri : null,
49325
+ typeof r2.mimeType === "string" ? r2.mimeType : null
49326
+ ].filter((v3) => v3 !== null);
49327
+ if (meta.length > 0)
49328
+ return `[resource:${meta.join(" ")}]`;
49329
+ }
49330
+ return `[resource]`;
49331
+ }
49260
49332
  var MCP_CALL_TIMEOUT_MS;
49261
49333
  var init_wrap = __esm({
49262
49334
  "../plugin-mcp/dist/wrap.js"() {
@@ -49264,13 +49336,16 @@ var init_wrap = __esm({
49264
49336
  MCP_CALL_TIMEOUT_MS = 5 * 60 * 1e3;
49265
49337
  }
49266
49338
  });
49267
- var mcpStoredServerSchema, mcpStoredConfigSchema;
49339
+ var mcpStoredServerSchema, mcpStoredConfigRootSchema;
49268
49340
  var init_config_schema = __esm({
49269
49341
  "../plugin-mcp/dist/admin/config-schema.js"() {
49270
49342
  mcpStoredServerSchema = z$1.object({
49271
49343
  name: z$1.string().min(1)
49272
49344
  }).passthrough();
49273
- mcpStoredConfigSchema = z$1.object({
49345
+ mcpStoredConfigRootSchema = z$1.object({
49346
+ servers: z$1.array(z$1.unknown())
49347
+ }).passthrough();
49348
+ z$1.object({
49274
49349
  servers: z$1.array(mcpStoredServerSchema)
49275
49350
  }).passthrough().transform((value) => value);
49276
49351
  }
@@ -49281,10 +49356,17 @@ function mcpConfigPath() {
49281
49356
  async function readMcpConfig() {
49282
49357
  try {
49283
49358
  const raw = await promises.readFile(mcpConfigPath(), "utf8");
49284
- const parsed = mcpStoredConfigSchema.safeParse(JSON.parse(raw));
49285
- if (parsed.success) {
49286
- return parsed.data;
49359
+ const root = mcpStoredConfigRootSchema.safeParse(JSON.parse(raw));
49360
+ if (!root.success) {
49361
+ return { servers: [] };
49287
49362
  }
49363
+ const servers = [];
49364
+ for (const entry of root.data.servers) {
49365
+ const parsed = mcpStoredServerSchema.safeParse(entry);
49366
+ if (parsed.success)
49367
+ servers.push(parsed.data);
49368
+ }
49369
+ return { servers };
49288
49370
  } catch {
49289
49371
  }
49290
49372
  return { servers: [] };
@@ -49917,23 +49999,19 @@ __export(dist_exports2, {
49917
49999
  });
49918
50000
  async function createMcpPlugin(opts) {
49919
50001
  const factory2 = opts.clientFactory ?? defaultClientFactory;
49920
- const clients = [];
49921
- const tools = [];
49922
- try {
49923
- for (const server of opts.servers) {
49924
- const client = await factory2(server, opts);
49925
- clients.push(client);
49926
- const wrapped = await wrapMcpServerTools({
49927
- server,
49928
- client,
49929
- toolNamePrefix: opts.toolNamePrefix
49930
- });
49931
- tools.push(...wrapped);
49932
- }
49933
- } catch (err) {
49934
- await Promise.allSettled(clients.map((c2) => c2.close()));
49935
- throw err;
50002
+ const opened = [];
50003
+ const results = await Promise.allSettled(opts.servers.map(async (server) => {
50004
+ const client = await factory2(server, opts);
50005
+ opened.push(client);
50006
+ return wrapMcpServerTools({ server, client, toolNamePrefix: opts.toolNamePrefix });
50007
+ }));
50008
+ const failure = results.find((r2) => r2.status === "rejected");
50009
+ if (failure) {
50010
+ await Promise.allSettled(opened.map((c2) => c2.close()));
50011
+ throw failure.reason;
49936
50012
  }
50013
+ const tools = results.flatMap((r2) => r2.value);
50014
+ const clients = opened;
49937
50015
  return definePlugin({
49938
50016
  name: "@moxxy/plugin-mcp",
49939
50017
  version: "0.0.0",
@@ -51297,7 +51375,7 @@ var require_react_development = __commonJS({
51297
51375
  var dispatcher = resolveDispatcher();
51298
51376
  return dispatcher.useRef(initialValue);
51299
51377
  }
51300
- function useEffect19(create2, deps) {
51378
+ function useEffect18(create2, deps) {
51301
51379
  var dispatcher = resolveDispatcher();
51302
51380
  return dispatcher.useEffect(create2, deps);
51303
51381
  }
@@ -52080,7 +52158,7 @@ var require_react_development = __commonJS({
52080
52158
  exports.useContext = useContext7;
52081
52159
  exports.useDebugValue = useDebugValue;
52082
52160
  exports.useDeferredValue = useDeferredValue;
52083
- exports.useEffect = useEffect19;
52161
+ exports.useEffect = useEffect18;
52084
52162
  exports.useId = useId;
52085
52163
  exports.useImperativeHandle = useImperativeHandle;
52086
52164
  exports.useInsertionEffect = useInsertionEffect;
@@ -84849,7 +84927,11 @@ function parseInputChunk(chunk, ctx) {
84849
84927
  continue;
84850
84928
  }
84851
84929
  if (c2 === "") {
84852
- process.exit(0);
84930
+ if (ctx.onInterrupt)
84931
+ ctx.onInterrupt();
84932
+ else
84933
+ process.exit(0);
84934
+ return remainder;
84853
84935
  }
84854
84936
  if (c2 === "\x7F" || c2 === "\b") {
84855
84937
  ctx.dispatch({ type: "delete-back" });
@@ -84973,7 +85055,7 @@ var init_PromptInput = __esm({
84973
85055
  init_external_insert();
84974
85056
  init_reducer();
84975
85057
  init_parse_input();
84976
- PromptInput = ({ onSubmit, disabled, placeholder, slashCommands = BUILTIN_SLASH_COMMANDS, onPasteText, commandHotkeys, onShiftTab, externalInsert }) => {
85058
+ PromptInput = ({ onSubmit, disabled, placeholder, slashCommands = BUILTIN_SLASH_COMMANDS, onPasteText, commandHotkeys, onShiftTab, onInterrupt, externalInsert }) => {
84977
85059
  const [state, dispatch3] = (0, import_react26.useReducer)(reducer, INITIAL);
84978
85060
  const [slashCursor, setSlashCursor] = import_react26.default.useState(0);
84979
85061
  const stateRef = (0, import_react26.useRef)(state);
@@ -85033,6 +85115,8 @@ var init_PromptInput = __esm({
85033
85115
  commandHotkeysRef.current = commandHotkeys;
85034
85116
  const onShiftTabRef = (0, import_react26.useRef)(onShiftTab);
85035
85117
  onShiftTabRef.current = onShiftTab;
85118
+ const onInterruptRef = (0, import_react26.useRef)(onInterrupt);
85119
+ onInterruptRef.current = onInterrupt;
85036
85120
  const lastExternalInsertIdRef = (0, import_react26.useRef)(null);
85037
85121
  (0, import_react26.useEffect)(() => {
85038
85122
  const decision = nextExternalInsertAction(lastExternalInsertIdRef.current, externalInsert);
@@ -85058,6 +85142,13 @@ var init_PromptInput = __esm({
85058
85142
  onSlashDown: () => setSlashCursor((c2) => Math.min(slashMatchesRef.current.length - 1, c2 + 1)),
85059
85143
  onSlashAccept: handleSlashAccept,
85060
85144
  onShiftTab: () => onShiftTabRef.current?.(),
85145
+ onInterrupt: () => {
85146
+ const fn = onInterruptRef.current;
85147
+ if (fn)
85148
+ fn();
85149
+ else
85150
+ process.exit(0);
85151
+ },
85061
85152
  onPasteText: (text) => onPasteTextRef.current?.(text) ?? text,
85062
85153
  slashOpen: false,
85063
85154
  bufferRef: { current: { buffer: "", cursor: 0 } },
@@ -85415,12 +85506,9 @@ function handleCollabEvent(e3, ref, root) {
85415
85506
  root.push(block2);
85416
85507
  return;
85417
85508
  }
85418
- let block = ref.current;
85419
- if (!block) {
85420
- block = newCollabBlock(e3.id, atMs);
85421
- ref.current = block;
85422
- root.push(block);
85423
- }
85509
+ const block = ref.current;
85510
+ if (!block)
85511
+ return;
85424
85512
  switch (e3.subtype) {
85425
85513
  case "collab_fallback_sequential":
85426
85514
  block.fallbackReason = String(p3.reason ?? "");
@@ -87122,8 +87210,37 @@ var init_image_attachments = __esm({
87122
87210
  };
87123
87211
  }
87124
87212
  });
87213
+ function reapStale(dir, now = Date.now()) {
87214
+ let entries;
87215
+ try {
87216
+ entries = readdirSync(dir);
87217
+ } catch {
87218
+ return;
87219
+ }
87220
+ for (const name of entries) {
87221
+ const m3 = CLIP_NAME_RE.exec(name);
87222
+ if (!m3)
87223
+ continue;
87224
+ const stamp = Number(m3[1]);
87225
+ let age = Number.isFinite(stamp) ? now - stamp : Number.NaN;
87226
+ if (!Number.isFinite(age)) {
87227
+ try {
87228
+ age = now - statSync(path3__default.join(dir, name)).mtimeMs;
87229
+ } catch {
87230
+ continue;
87231
+ }
87232
+ }
87233
+ if (age > CACHE_TTL_MS) {
87234
+ try {
87235
+ unlinkSync(path3__default.join(dir, name));
87236
+ } catch {
87237
+ }
87238
+ }
87239
+ }
87240
+ }
87125
87241
  function ensureCacheDir() {
87126
87242
  mkdirSync(CACHE_DIR, { recursive: true });
87243
+ reapStale(CACHE_DIR, Date.now());
87127
87244
  return CACHE_DIR;
87128
87245
  }
87129
87246
  function nextCachePath() {
@@ -87208,10 +87325,12 @@ function readClipboardImageSync() {
87208
87325
  return readClipboardImageLinux();
87209
87326
  return null;
87210
87327
  }
87211
- var CACHE_DIR;
87328
+ var CACHE_DIR, CACHE_TTL_MS, CLIP_NAME_RE;
87212
87329
  var init_clipboard_image = __esm({
87213
87330
  "../plugin-cli/dist/clipboard-image.js"() {
87214
87331
  CACHE_DIR = moxxyPath("image-cache");
87332
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
87333
+ CLIP_NAME_RE = /^clip-(\d+)-[a-z0-9]+\.png$/;
87215
87334
  }
87216
87335
  });
87217
87336
 
@@ -87641,7 +87760,7 @@ function useVoiceInput(opts) {
87641
87760
  const pcm = await recording.stop();
87642
87761
  const transcriber = resolveCodexTranscriber(session);
87643
87762
  const result = await transcriber.transcribe(pcm, {
87644
- mimeType: MOXXY_PCM16_24KHZ_MIME2
87763
+ mimeType: MOXXY_PCM16_24KHZ_MIME
87645
87764
  });
87646
87765
  const text = result.text.trim();
87647
87766
  if (!text) {
@@ -87748,13 +87867,12 @@ function formatVoiceError(err) {
87748
87867
  return `voice: ${message}`;
87749
87868
  return `voice: ${message || "failed"}`;
87750
87869
  }
87751
- var import_react51, CODEX_TRANSCRIBER_NAME, MOXXY_PCM16_24KHZ_MIME2;
87870
+ var import_react51, CODEX_TRANSCRIBER_NAME;
87752
87871
  var init_use_voice_input = __esm({
87753
87872
  "../plugin-cli/dist/session/use-voice-input.js"() {
87754
87873
  import_react51 = __toESM(require_react());
87755
87874
  init_voice_input();
87756
87875
  CODEX_TRANSCRIBER_NAME = "openai-codex-transcribe";
87757
- MOXXY_PCM16_24KHZ_MIME2 = "audio/x-moxxy-pcm16-24khz";
87758
87876
  }
87759
87877
  });
87760
87878
 
@@ -88044,13 +88162,17 @@ function startCollab(deps, arg) {
88044
88162
  deps.submitPrompt(objective);
88045
88163
  }
88046
88164
  function openModePicker(deps, arg = "") {
88047
- const modes = deps.session.modes.list();
88165
+ const modes = deps.session.modes.list().filter((m3) => !COLLAB_HIDDEN_MODES.has(m3.name));
88048
88166
  if (modes.length === 0) {
88049
88167
  deps.setSystemNotice("no modes registered");
88050
88168
  return;
88051
88169
  }
88052
88170
  const target = arg.trim().toLowerCase();
88053
88171
  if (target) {
88172
+ if (COLLAB_HIDDEN_MODES.has(target)) {
88173
+ deps.setSystemNotice("Use /collab <goal> to run a collaborative team (only one runs at a time).");
88174
+ return;
88175
+ }
88054
88176
  const match = modes.find((m3) => m3.name.toLowerCase() === target);
88055
88177
  if (match) {
88056
88178
  try {
@@ -88076,7 +88198,7 @@ function openModePicker(deps, arg = "") {
88076
88198
  function truncate5(s2, n2) {
88077
88199
  return s2.length <= n2 ? s2 : s2.slice(0, n2 - 1) + "\u2026";
88078
88200
  }
88079
- var PLUGIN_KIND_TAB, OTHER_TAB, PLUGIN_TAB_ORDER;
88201
+ var PLUGIN_KIND_TAB, OTHER_TAB, PLUGIN_TAB_ORDER, COLLAB_HIDDEN_MODES;
88080
88202
  var init_run_slash = __esm({
88081
88203
  async "../plugin-cli/dist/session/run-slash.js"() {
88082
88204
  init_dist();
@@ -88091,6 +88213,11 @@ var init_run_slash = __esm({
88091
88213
  };
88092
88214
  OTHER_TAB = { id: "others", label: "Others" };
88093
88215
  PLUGIN_TAB_ORDER = ["providers", "modes", "channels", "tools", "others"];
88216
+ COLLAB_HIDDEN_MODES = /* @__PURE__ */ new Set([
88217
+ "collaborative",
88218
+ "collab-architect",
88219
+ "collab-peer"
88220
+ ]);
88094
88221
  }
88095
88222
  });
88096
88223
 
@@ -89063,11 +89190,11 @@ var init_OverlayOrNotice = __esm({
89063
89190
  });
89064
89191
 
89065
89192
  // ../plugin-cli/dist/components/PermissionDialog.js
89066
- var import_jsx_runtime32, import_react60, PermissionDialog;
89193
+ var import_jsx_runtime32, PermissionDialog;
89067
89194
  var init_PermissionDialog = __esm({
89068
89195
  async "../plugin-cli/dist/components/PermissionDialog.js"() {
89069
89196
  import_jsx_runtime32 = __toESM(require_jsx_runtime());
89070
- import_react60 = __toESM(require_react());
89197
+ __toESM(require_react());
89071
89198
  await init_build2();
89072
89199
  init_theme();
89073
89200
  await init_Modal();
@@ -89083,8 +89210,6 @@ var init_PermissionDialog = __esm({
89083
89210
  else if (ch === "n" || key.escape)
89084
89211
  onDecide({ mode: "deny", reason: "user declined" });
89085
89212
  });
89086
- (0, import_react60.useEffect)(() => {
89087
- }, []);
89088
89213
  const title = queueDepth > 0 ? `Tool permission requested (${queueDepth} more queued)` : "Tool permission requested";
89089
89214
  return (0, import_jsx_runtime32.jsxs)(Modal, { title, hints: "y allow \xB7 a session \xB7 p always \xB7 n deny", children: [(0, import_jsx_runtime32.jsxs)(Text, { children: ["Tool: ", (0, import_jsx_runtime32.jsx)(Text, { bold: true, children: call.name }), toolDescription ? (0, import_jsx_runtime32.jsxs)(Text, { dimColor: true, children: [" \u2014 ", toolDescription] }) : null] }), (0, import_jsx_runtime32.jsxs)(Text, { dimColor: true, children: ["Input: ", JSON.stringify(call.input).slice(0, 200)] }), (0, import_jsx_runtime32.jsxs)(Text, { children: [(0, import_jsx_runtime32.jsx)(Text, { children: "[y]" }), (0, import_jsx_runtime32.jsx)(Text, { dimColor: true, children: " allow once \xB7 " }), (0, import_jsx_runtime32.jsx)(Text, { children: "[a]" }), (0, import_jsx_runtime32.jsx)(Text, { dimColor: true, children: " allow session \xB7 " }), (0, import_jsx_runtime32.jsx)(Text, { children: "[p]" }), (0, import_jsx_runtime32.jsx)(Text, { dimColor: true, children: " always \xB7 " }), (0, import_jsx_runtime32.jsx)(Text, { color: Colors.danger, children: "[n]" }), (0, import_jsx_runtime32.jsx)(Text, { dimColor: true, children: " deny" })] })] });
89090
89215
  };
@@ -89092,6 +89217,11 @@ var init_PermissionDialog = __esm({
89092
89217
  });
89093
89218
 
89094
89219
  // ../plugin-cli/dist/components/ApprovalDialog.js
89220
+ function jkScrolls(letter, renderedLineCount, options) {
89221
+ if (renderedLineCount <= MAX_BODY_LINES)
89222
+ return false;
89223
+ return !options.some((o2) => o2.hotkey === letter);
89224
+ }
89095
89225
  function isDiffBody(body) {
89096
89226
  return /```diff\b/.test(body) || /^\s*diff --git\b/m.test(body);
89097
89227
  }
@@ -89148,6 +89278,10 @@ var init_ApprovalDialog = __esm({
89148
89278
  const [cursor, setCursor] = (0, import_react61.useState)(initialCursor);
89149
89279
  const [textEntry, setTextEntry] = (0, import_react61.useState)(null);
89150
89280
  const [scrollOffset, setScrollOffset] = (0, import_react61.useState)(0);
89281
+ const rawLines = request.body.split("\n");
89282
+ const diffMode = isDiffBody(request.body);
89283
+ const rendered = diffMode ? renderDiffLines(rawLines) : rawLines.map((text) => ({ text }));
89284
+ const scrollClaims = (letter) => jkScrolls(letter, rendered.length, request.options);
89151
89285
  use_input_default((input, key) => {
89152
89286
  if (textEntry) {
89153
89287
  if (key.return) {
@@ -89171,11 +89305,12 @@ var init_ApprovalDialog = __esm({
89171
89305
  }
89172
89306
  return;
89173
89307
  }
89174
- if (input === "j" || key.pageDown) {
89175
- setScrollOffset((o2) => o2 + Math.max(1, Math.floor(MAX_BODY_LINES / 2)));
89308
+ const maxScroll = Math.max(0, rendered.length - MAX_BODY_LINES);
89309
+ if (input === "j" && scrollClaims("j") || key.pageDown) {
89310
+ setScrollOffset((o2) => Math.min(maxScroll, o2 + Math.max(1, Math.floor(MAX_BODY_LINES / 2))));
89176
89311
  return;
89177
89312
  }
89178
- if (input === "k" || key.pageUp) {
89313
+ if (input === "k" && scrollClaims("k") || key.pageUp) {
89179
89314
  setScrollOffset((o2) => Math.max(0, o2 - Math.max(1, Math.floor(MAX_BODY_LINES / 2))));
89180
89315
  return;
89181
89316
  }
@@ -89223,9 +89358,6 @@ var init_ApprovalDialog = __esm({
89223
89358
  }
89224
89359
  onDecide({ optionId: id });
89225
89360
  };
89226
- const rawLines = request.body.split("\n");
89227
- const diffMode = isDiffBody(request.body);
89228
- const rendered = diffMode ? renderDiffLines(rawLines) : rawLines.map((text) => ({ text }));
89229
89361
  const maxOffset = Math.max(0, rendered.length - MAX_BODY_LINES);
89230
89362
  const offset = Math.min(scrollOffset, maxOffset);
89231
89363
  const visible = rendered.slice(offset, offset + MAX_BODY_LINES);
@@ -113398,7 +113530,7 @@ async function provisionWorkspace(opts) {
113398
113530
  if (install.code !== 0) {
113399
113531
  const loose = await run("pnpm", ["install"], dir);
113400
113532
  if (loose.code !== 0)
113401
- return { ok: false, message: `pnpm install failed: ${trunc(install.output, 400)}`, repoDir: dir };
113533
+ return { ok: false, message: `pnpm install failed: ${trunc(loose.output, 400)}`, repoDir: dir };
113402
113534
  }
113403
113535
  return { ok: true, message: "workspace provisioned", repoDir: dir };
113404
113536
  }
@@ -114031,6 +114163,23 @@ async function escalate(deps, ctx, journal, reason) {
114031
114163
  }
114032
114164
 
114033
114165
  // src/argv.ts
114166
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
114167
+ "help",
114168
+ "h",
114169
+ "version",
114170
+ "v",
114171
+ "yes",
114172
+ "y",
114173
+ "verbose",
114174
+ "allow-all",
114175
+ "standalone",
114176
+ "attach",
114177
+ "reload",
114178
+ "all",
114179
+ "stop",
114180
+ "status",
114181
+ "background"
114182
+ ]);
114034
114183
  function parseArgv(argv) {
114035
114184
  const result = { command: "", flags: {}, positional: [] };
114036
114185
  if (argv.length === 0) {
@@ -114053,7 +114202,7 @@ function parseArgv(argv) {
114053
114202
  } else {
114054
114203
  const key = a2.slice(2);
114055
114204
  const next = argv[i2 + 1];
114056
- if (next && !next.startsWith("-")) {
114205
+ if (next && !next.startsWith("-") && !BOOLEAN_FLAGS.has(key)) {
114057
114206
  result.flags[key] = next;
114058
114207
  i2++;
114059
114208
  } else {
@@ -114063,7 +114212,7 @@ function parseArgv(argv) {
114063
114212
  } else if (a2.startsWith("-")) {
114064
114213
  const key = a2.slice(1);
114065
114214
  const next = argv[i2 + 1];
114066
- if (next && !next.startsWith("-")) {
114215
+ if (next && !next.startsWith("-") && !BOOLEAN_FLAGS.has(key)) {
114067
114216
  result.flags[key] = next;
114068
114217
  i2++;
114069
114218
  } else {
@@ -114317,17 +114466,19 @@ async function loadOne(filePath) {
114317
114466
  }
114318
114467
  return parsed.data;
114319
114468
  }
114320
- var cachedJiti = null;
114469
+ var cachedJiti = /* @__PURE__ */ new Map();
114321
114470
  async function getJiti2(cwd2) {
114322
- if (cachedJiti)
114323
- return cachedJiti;
114471
+ const existing = cachedJiti.get(cwd2);
114472
+ if (existing)
114473
+ return existing;
114324
114474
  try {
114325
114475
  const mod = await Promise.resolve().then(() => (init_jiti(), jiti_exports));
114326
114476
  const factory2 = mod.createJiti ?? mod.default;
114327
114477
  if (!factory2)
114328
114478
  return null;
114329
- cachedJiti = factory2(cwd2, { interopDefault: true });
114330
- return cachedJiti;
114479
+ const instance = factory2(cwd2, { interopDefault: true });
114480
+ cachedJiti.set(cwd2, instance);
114481
+ return instance;
114331
114482
  } catch {
114332
114483
  return null;
114333
114484
  }
@@ -114586,9 +114737,22 @@ function decrypt(blob, key) {
114586
114737
  return plaintext.toString("utf8");
114587
114738
  }
114588
114739
  function randomCode(digits = 6) {
114589
- const max = 10 ** digits;
114590
- const value = randomBytes(4).readUInt32BE(0) % max;
114591
- return value.toString().padStart(digits, "0");
114740
+ if (!Number.isInteger(digits) || digits < 1) {
114741
+ throw new Error(`randomCode: digits must be a positive integer, got ${digits}`);
114742
+ }
114743
+ const modulus = 10n ** BigInt(digits);
114744
+ const byteLen = Math.ceil(digits * Math.log2(10) / 8) + 1;
114745
+ const space = 1n << BigInt(byteLen * 8);
114746
+ const limit2 = space - space % modulus;
114747
+ for (; ; ) {
114748
+ let value = 0n;
114749
+ for (const byte of randomBytes(byteLen)) {
114750
+ value = value << 8n | BigInt(byte);
114751
+ }
114752
+ if (value < limit2) {
114753
+ return (value % modulus).toString().padStart(digits, "0");
114754
+ }
114755
+ }
114592
114756
  }
114593
114757
 
114594
114758
  // ../plugin-vault/dist/keysource.js
@@ -114609,10 +114773,10 @@ function createCombinedKeySource(opts) {
114609
114773
  },
114610
114774
  async obtain(salt) {
114611
114775
  const envName = opts.envVar ?? "MOXXY_VAULT_PASSPHRASE";
114612
- const envValue = process.env[envName];
114613
- if (envValue) {
114776
+ const envValue2 = process.env[envName];
114777
+ if (envValue2) {
114614
114778
  resolvedName = `env:${envName}`;
114615
- return deriveKey(envValue, salt);
114779
+ return deriveKey(envValue2, salt);
114616
114780
  }
114617
114781
  if (!opts.disableKeytar) {
114618
114782
  const fromKeychain = await tryKeychainGet();
@@ -115404,7 +115568,7 @@ async function safeRead(filePath) {
115404
115568
  const result = memoryFrontmatterSchema.safeParse(parsed.frontmatter);
115405
115569
  if (!result.success)
115406
115570
  return null;
115407
- return { frontmatter: result.data, body: parsed.body };
115571
+ return { frontmatter: result.data, body: parsed.body.trim() };
115408
115572
  } catch {
115409
115573
  return null;
115410
115574
  }
@@ -117036,6 +117200,14 @@ async function syncSkillSchedules(registry, store) {
117036
117200
  }
117037
117201
 
117038
117202
  // ../plugin-scheduler/dist/poller.js
117203
+ function cronBaseline(entry) {
117204
+ return entry.lastRunAt ?? entry.createdAt;
117205
+ }
117206
+ function nextCronFire(entry) {
117207
+ if (!entry.cron)
117208
+ return null;
117209
+ return nextFireTime(entry.cron, new Date(cronBaseline(entry)), entry.timeZone);
117210
+ }
117039
117211
  function isDue(entry, now) {
117040
117212
  if (!entry.enabled)
117041
117213
  return false;
@@ -117044,8 +117216,7 @@ function isDue(entry, now) {
117044
117216
  }
117045
117217
  if (!entry.cron)
117046
117218
  return false;
117047
- const since = entry.lastRunAt ?? entry.createdAt;
117048
- const next = nextFireTime(entry.cron, new Date(since), entry.timeZone);
117219
+ const next = nextCronFire(entry);
117049
117220
  if (!next)
117050
117221
  return false;
117051
117222
  return next.getTime() <= now;
@@ -117079,23 +117250,12 @@ var SchedulerPoller = class {
117079
117250
  await this.tickPromise.catch(() => void 0);
117080
117251
  }
117081
117252
  /** Fire any due schedules right now, ignoring the timer cadence.
117082
- * Returns the number of schedules that ran. */
117253
+ * Returns the number of due schedules that were attempted — counted at the
117254
+ * attempt point, so a schedule that fired but failed mid-run (e.g. a
117255
+ * store.update throw) is still counted, unlike piggy-backing on onFired
117256
+ * (which only fires on a clean run). */
117083
117257
  async tickOnce() {
117084
- let count = 0;
117085
- const original = this.opts.onFired;
117086
- let wrapped;
117087
- if (original) {
117088
- wrapped = (entry, outcome) => {
117089
- count += 1;
117090
- original(entry, outcome);
117091
- };
117092
- } else {
117093
- wrapped = () => {
117094
- count += 1;
117095
- };
117096
- }
117097
- await this.tickWith(wrapped);
117098
- return count;
117258
+ return this.tickWith(this.opts.onFired);
117099
117259
  }
117100
117260
  async tick() {
117101
117261
  if (!this.running)
@@ -117120,11 +117280,13 @@ var SchedulerPoller = class {
117120
117280
  this.opts.logger?.error?.("scheduler: failed to read store", {
117121
117281
  err: err instanceof Error ? err.message : String(err)
117122
117282
  });
117123
- return;
117283
+ return 0;
117124
117284
  }
117285
+ let attempted = 0;
117125
117286
  for (const entry of schedules) {
117126
117287
  if (!isDue(entry, now))
117127
117288
  continue;
117289
+ attempted += 1;
117128
117290
  try {
117129
117291
  const outcome = await runSchedule(entry, this.opts.runner, this.opts.store, this.opts.inbox);
117130
117292
  this.opts.logger?.info?.("scheduler: fired", {
@@ -117140,6 +117302,7 @@ var SchedulerPoller = class {
117140
117302
  });
117141
117303
  }
117142
117304
  }
117305
+ return attempted;
117143
117306
  }
117144
117307
  };
117145
117308
 
@@ -117382,11 +117545,9 @@ var cronOrTimestamp = z$1.object({
117382
117545
  message: "provide either `cron` or `runAt`"
117383
117546
  });
117384
117547
  function describeEntry(entry) {
117385
- const now = Date.now();
117386
117548
  let nextFireAt = null;
117387
117549
  if (entry.cron) {
117388
- const since = entry.lastRunAt ?? Math.max(entry.createdAt, now);
117389
- const next = nextFireTime(entry.cron, new Date(since), entry.timeZone);
117550
+ const next = nextCronFire(entry);
117390
117551
  nextFireAt = next ? next.getTime() : null;
117391
117552
  } else if (entry.runAt && entry.enabled) {
117392
117553
  nextFireAt = entry.runAt;
@@ -117680,7 +117841,8 @@ function defaultWebhookInboxDir() {
117680
117841
  async function writeInbox2(trigger, result, deliveryId, opts = {}) {
117681
117842
  const dir = opts.dir ?? defaultWebhookInboxDir();
117682
117843
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
117683
- const file = path3__default.join(dir, `${stamp}-${trigger.name}.md`);
117844
+ const suffix = deliveryId ? deliveryId.replace(/[^A-Za-z0-9._-]/g, "_") : randomUUID().slice(0, 8);
117845
+ const file = path3__default.join(dir, `${stamp}-${trigger.name}-${suffix}.md`);
117684
117846
  const header = [
117685
117847
  "---",
117686
117848
  `webhook: ${trigger.name}`,
@@ -118047,14 +118209,6 @@ var WebhookServer = class {
118047
118209
  res.end(JSON.stringify({ error: "verification_failed" }));
118048
118210
  return;
118049
118211
  }
118050
- if (!shouldFire(trigger.filters, { headers: req.headers, body })) {
118051
- this.opts.logger?.info?.("webhooks: filtered out, not firing", {
118052
- trigger: trigger.name
118053
- });
118054
- res.writeHead(200, { "content-type": "application/json" });
118055
- res.end(JSON.stringify({ status: "filtered" }));
118056
- return;
118057
- }
118058
118212
  const idempKey = idempotencyKey(trigger, req.headers);
118059
118213
  if (idempKey && !this.dedupe.check(trigger.id, idempKey)) {
118060
118214
  this.opts.logger?.info?.("webhooks: duplicate delivery, dropped", {
@@ -118065,6 +118219,14 @@ var WebhookServer = class {
118065
118219
  res.end(JSON.stringify({ status: "duplicate" }));
118066
118220
  return;
118067
118221
  }
118222
+ if (!shouldFire(trigger.filters, { headers: req.headers, body })) {
118223
+ this.opts.logger?.info?.("webhooks: filtered out, not firing", {
118224
+ trigger: trigger.name
118225
+ });
118226
+ res.writeHead(200, { "content-type": "application/json" });
118227
+ res.end(JSON.stringify({ status: "filtered" }));
118228
+ return;
118229
+ }
118068
118230
  res.writeHead(202, { "content-type": "application/json" });
118069
118231
  res.end(JSON.stringify({ status: "accepted", trigger: trigger.name }));
118070
118232
  const prompt = renderPrompt({
@@ -119138,17 +119300,28 @@ function isPathKey(key) {
119138
119300
  function isUrlKey(key) {
119139
119301
  return tokenize4(key).some((w4) => URL_WORDS.has(w4));
119140
119302
  }
119303
+ var INVALID_FILE_URL_PATH = "/\0moxxy-invalid-file-url";
119141
119304
  function extractPaths(input) {
119142
119305
  const out = [];
119143
119306
  walkStrings(input, (key, value) => {
119144
- if (isPathKey(key)) {
119307
+ if (value.startsWith("file://")) {
119308
+ out.push(fileUrlPath(value));
119309
+ } else if (isPathKey(key)) {
119145
119310
  out.push(value);
119146
- } else if (value.startsWith("file://")) {
119147
- out.push(value.slice("file://".length));
119148
119311
  }
119149
119312
  });
119150
119313
  return out;
119151
119314
  }
119315
+ function fileUrlPath(value) {
119316
+ try {
119317
+ const url2 = new URL(value);
119318
+ if (url2.host)
119319
+ return INVALID_FILE_URL_PATH;
119320
+ return decodeURIComponent(url2.pathname);
119321
+ } catch {
119322
+ return INVALID_FILE_URL_PATH;
119323
+ }
119324
+ }
119152
119325
  function extractUrls(input) {
119153
119326
  const out = [];
119154
119327
  walkStrings(input, (key, value) => {
@@ -119181,11 +119354,20 @@ function resolvePattern(pattern, cwd2) {
119181
119354
  }
119182
119355
  return path3.isAbsolute(pattern) ? path3.normalize(pattern) : path3.resolve(cwd2, pattern);
119183
119356
  }
119357
+ var globRegexCache = /* @__PURE__ */ new Map();
119358
+ function compileGlob(pattern) {
119359
+ let re2 = globRegexCache.get(pattern);
119360
+ if (!re2) {
119361
+ const src = "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "<<DOUBLESTAR>>").replace(/\*/g, "[^/]*").replace(/<<DOUBLESTAR>>/g, ".*") + "$";
119362
+ re2 = new RegExp(src);
119363
+ globRegexCache.set(pattern, re2);
119364
+ }
119365
+ return re2;
119366
+ }
119184
119367
  function matchesGlob(p3, pattern) {
119185
119368
  if (pattern.endsWith("/**") && p3 === pattern.slice(0, -3))
119186
119369
  return true;
119187
- const re2 = "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "<<DOUBLESTAR>>").replace(/\*/g, "[^/]*").replace(/<<DOUBLESTAR>>/g, ".*") + "$";
119188
- return new RegExp(re2).test(p3);
119370
+ return compileGlob(pattern).test(p3);
119189
119371
  }
119190
119372
  function hostMatches(host, pattern) {
119191
119373
  if (pattern.startsWith("*.")) {
@@ -119297,6 +119479,7 @@ var IsolatorRegistry2 = class {
119297
119479
  }
119298
119480
  };
119299
119481
  var MAX_BROKER_OUTPUT_BYTES = 8 * 1024 * 1024;
119482
+ var BROKER_KILL_GRACE_MS = 2e3;
119300
119483
  var MAX_FETCH_REDIRECTS = 5;
119301
119484
  var BLOCKED_HANDLER_MODULES = Object.freeze([
119302
119485
  "node:fs",
@@ -119589,12 +119772,21 @@ async function brokerExec(args, { caps, cwd: cwd2, signal }) {
119589
119772
  const errChunks = [];
119590
119773
  let total = 0;
119591
119774
  let settled = false;
119592
- const timer = opts.timeoutMs ? setTimeout(() => {
119775
+ let killTimer = null;
119776
+ const terminate2 = () => {
119593
119777
  child.kill("SIGTERM");
119778
+ if (killTimer)
119779
+ return;
119780
+ killTimer = setTimeout(() => child.kill("SIGKILL"), BROKER_KILL_GRACE_MS);
119781
+ killTimer.unref?.();
119782
+ };
119783
+ const timer = opts.timeoutMs ? setTimeout(() => {
119784
+ terminate2();
119594
119785
  finish(() => reject(new Error(`[broker:exec] '${command}' exceeded ${opts.timeoutMs}ms`)));
119595
119786
  }, opts.timeoutMs) : null;
119596
119787
  const onAbort = () => {
119597
- child.kill("SIGTERM");
119788
+ terminate2();
119789
+ finish(() => reject(new Error(`[broker:exec] '${command}' aborted`)));
119598
119790
  };
119599
119791
  const finish = (settle) => {
119600
119792
  if (settled)
@@ -119605,12 +119797,18 @@ async function brokerExec(args, { caps, cwd: cwd2, signal }) {
119605
119797
  signal.removeEventListener("abort", onAbort);
119606
119798
  settle();
119607
119799
  };
119800
+ const clearKill = () => {
119801
+ if (killTimer) {
119802
+ clearTimeout(killTimer);
119803
+ killTimer = null;
119804
+ }
119805
+ };
119608
119806
  const accumulate = (chunks, b3) => {
119609
119807
  if (settled)
119610
119808
  return;
119611
119809
  total += b3.byteLength;
119612
119810
  if (total > MAX_BROKER_OUTPUT_BYTES) {
119613
- child.kill("SIGTERM");
119811
+ terminate2();
119614
119812
  finish(() => reject(new Error(`[broker:exec] '${command}' output exceeded the ${MAX_BROKER_OUTPUT_BYTES}-byte limit`)));
119615
119813
  return;
119616
119814
  }
@@ -119620,9 +119818,11 @@ async function brokerExec(args, { caps, cwd: cwd2, signal }) {
119620
119818
  child.stderr.on("data", (b3) => accumulate(errChunks, b3));
119621
119819
  signal.addEventListener("abort", onAbort, { once: true });
119622
119820
  child.on("error", (e3) => {
119821
+ clearKill();
119623
119822
  finish(() => reject(e3));
119624
119823
  });
119625
119824
  child.on("close", (exitCode) => {
119825
+ clearKill();
119626
119826
  finish(() => resolve12({
119627
119827
  stdout: Buffer.concat(outChunks).toString("utf8"),
119628
119828
  stderr: Buffer.concat(errChunks).toString("utf8"),
@@ -119899,7 +120099,8 @@ function createWorkerIsolator(opts = {}) {
119899
120099
  let terminated = false;
119900
120100
  const hardTerminate = () => {
119901
120101
  terminated = true;
119902
- void worker.terminate();
120102
+ worker.terminate().catch(() => {
120103
+ });
119903
120104
  };
119904
120105
  const finish = (action, graceful = false) => {
119905
120106
  if (settled)
@@ -120263,6 +120464,7 @@ async function invoke(call, caps, _signal) {
120263
120464
  if (typeof exports.alloc !== "function") {
120264
120465
  throw new Error(`[security:wasm] module does not export 'alloc(size: i32) -> i32'`);
120265
120466
  }
120467
+ memoryHolder.alloc = (size) => exports.alloc(size);
120266
120468
  const handler = exports[call.moduleRef.export];
120267
120469
  if (typeof handler !== "function") {
120268
120470
  throw new Error(`[security:wasm] export '${call.moduleRef.export}' is ${typeof handler}, expected function`);
@@ -120295,8 +120497,10 @@ function buildWasmHostImports(memoryHolder, caps, cwd2) {
120295
120497
  return new TextDecoder().decode(new Uint8Array(memOf().buffer, ptr, len));
120296
120498
  };
120297
120499
  const sendBytes = (outPtrOut, outLenOut, bytes) => {
120298
- const region = reserveScratch(memOf(), bytes.length);
120299
- new Uint8Array(memOf().buffer, region, bytes.length).set(bytes);
120500
+ const region = bytes.length === 0 ? SCRATCH_BASE : memoryHolder.alloc ? memoryHolder.alloc(bytes.length) : reserveScratch(memOf(), bytes.length);
120501
+ if (bytes.length > 0) {
120502
+ new Uint8Array(memOf().buffer, region, bytes.length).set(bytes);
120503
+ }
120300
120504
  writePtrPair(memOf(), outPtrOut, outLenOut, region, bytes.length);
120301
120505
  };
120302
120506
  const sendStr = (outPtrOut, outLenOut, s2) => {
@@ -120337,13 +120541,13 @@ function buildWasmHostImports(memoryHolder, caps, cwd2) {
120337
120541
  */
120338
120542
  broker_fs_write_file: (pathPtr, pathLen, dataPtr, dataLen, outPtrOut, outLenOut) => {
120339
120543
  const filePath = readStr(pathPtr, pathLen);
120340
- const data = readStr(dataPtr, dataLen);
120544
+ const data = new Uint8Array(memOf().buffer, dataPtr, dataLen).slice();
120341
120545
  if (!pathInScope(filePath, caps.fs, cwd2, "write")) {
120342
120546
  return sendErr(outPtrOut, outLenOut, `[broker:fs.writeFile] path '${filePath}' is outside the tool's declared fs.write capability`);
120343
120547
  }
120344
120548
  try {
120345
120549
  mkdirSync(path3.dirname(filePath), { recursive: true });
120346
- writeFileSync(filePath, data, "utf8");
120550
+ writeFileSync(filePath, Buffer$1.from(data));
120347
120551
  writePtrPair(memOf(), outPtrOut, outLenOut, 0, 0);
120348
120552
  return SUCCESS;
120349
120553
  } catch (e3) {
@@ -124450,6 +124654,8 @@ var AnthropicProvider = class {
124450
124654
  const parts = [this.oauth?.systemPreamble, system, req.system].filter((s2) => typeof s2 === "string" && s2.length > 0);
124451
124655
  const systemForCount = parts.length > 0 ? parts.join("\n\n") : void 0;
124452
124656
  try {
124657
+ if (this.oauth)
124658
+ await this.ensureFreshOauth();
124453
124659
  const result = await this.client.messages.countTokens({
124454
124660
  model: req.model || this.defaultModel,
124455
124661
  ...systemForCount !== void 0 ? { system: systemForCount } : {},
@@ -130898,7 +131104,16 @@ var OpenAIProvider = class {
130898
131104
  ...tools ? { tools } : {},
130899
131105
  ...req.temperature !== void 0 ? { temperature: req.temperature } : {},
130900
131106
  ...req.maxTokens ? { [tokenLimitKey]: req.maxTokens } : {},
130901
- ...emitReasoning && usesCompletionTokens && reasoningEffort ? { reasoning_effort: reasoningEffort } : {},
131107
+ // Send `reasoning_effort` independently of the token-field heuristic.
131108
+ // The two are unrelated concerns: `usesCompletionTokens` picks the cap
131109
+ // FIELD NAME for the OpenAI-hosted reasoning models, while effort applies
131110
+ // to any reasoning backend. OpenAI-compatible vendors (z.ai GLM,
131111
+ // DeepSeek-R1, vLLM, Ollama) honor reasoning_effort but their model ids
131112
+ // never match the gpt-5/o1/o3 regex, so gating effort on it silently
131113
+ // dropped a user-requested effort for exactly those backends. The
131114
+ // descriptor's `supportsReasoning` already gates this upstream via
131115
+ // req.reasoning.
131116
+ ...emitReasoning && reasoningEffort ? { reasoning_effort: reasoningEffort } : {},
130902
131117
  stream: true,
130903
131118
  // OpenAI only emits the final `usage` chunk when this is set;
130904
131119
  // without it `raw.usage` is null on every chunk and token usage
@@ -131162,7 +131377,6 @@ function waitForCallback(opts) {
131162
131377
  if (err) {
131163
131378
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
131164
131379
  res.end(htmlPage("OAuth error", `${err}${errDesc ? `: ${errDesc}` : ""}`));
131165
- clearTimeout(timer);
131166
131380
  const denied = err === "access_denied";
131167
131381
  settle(() => reject(new MoxxyError({
131168
131382
  code: denied ? "OAUTH_FLOW_DENIED" : "AUTH_INVALID",
@@ -131177,7 +131391,6 @@ function waitForCallback(opts) {
131177
131391
  if (!code || !returnedState) {
131178
131392
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
131179
131393
  res.end(htmlPage("OAuth error", "callback was missing code or state"));
131180
- clearTimeout(timer);
131181
131394
  settle(() => reject(new MoxxyError({
131182
131395
  code: "AUTH_INVALID",
131183
131396
  message: "OAuth callback was missing code or state \u2014 the upstream redirect is malformed.",
@@ -131188,7 +131401,6 @@ function waitForCallback(opts) {
131188
131401
  if (returnedState !== opts.expectedState) {
131189
131402
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
131190
131403
  res.end(htmlPage("OAuth error", "state mismatch \u2014 possible CSRF, refusing"));
131191
- clearTimeout(timer);
131192
131404
  settle(() => reject(new MoxxyError({
131193
131405
  code: "OAUTH_FLOW_STATE_MISMATCH",
131194
131406
  message: "OAuth state mismatch \u2014 possible CSRF attempt, refusing to continue.",
@@ -131246,7 +131458,8 @@ async function exchangeCodeForToken(input, fetchImpl2 = fetch) {
131246
131458
  const res = await fetchImpl2(input.tokenUrl, {
131247
131459
  method: "POST",
131248
131460
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
131249
- body: body.toString()
131461
+ body: body.toString(),
131462
+ ...input.signal ? { signal: input.signal } : {}
131250
131463
  });
131251
131464
  if (!res.ok) {
131252
131465
  const text = await res.text().catch(() => "");
@@ -131362,7 +131575,10 @@ async function runAuthorizationCodeFlow(opts) {
131362
131575
  });
131363
131576
  }
131364
131577
  async function pollUntil(fn, opts) {
131365
- const state = { intervalMs: opts.intervalMs };
131578
+ const state = {
131579
+ intervalMs: opts.intervalMs,
131580
+ ...opts.signal ? { signal: opts.signal } : {}
131581
+ };
131366
131582
  const leadingWait = opts.leadingWait ?? true;
131367
131583
  const deadline = Date.now() + opts.timeoutMs;
131368
131584
  const label3 = opts.label ?? "poll";
@@ -131512,7 +131728,8 @@ async function pollOnce(opts, deviceCode, state) {
131512
131728
  const pollRes = await fetch(opts.tokenUrl, {
131513
131729
  method: "POST",
131514
131730
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
131515
- body: body.toString()
131731
+ body: body.toString(),
131732
+ ...state.signal ? { signal: state.signal } : {}
131516
131733
  });
131517
131734
  const pollJson = await pollRes.json().catch(() => ({}));
131518
131735
  return classifyDeviceTokenResponse(pollRes, pollJson, state);
@@ -132110,12 +132327,13 @@ function openaiDeviceFlow(opts) {
132110
132327
  }
132111
132328
  };
132112
132329
  },
132113
- async poll(init3, _state) {
132330
+ async poll(init3, state) {
132114
132331
  const { deviceAuthId, userCode, clientId } = init3.providerData;
132115
132332
  const res = await fetch(pollUrl, {
132116
132333
  method: "POST",
132117
132334
  headers: { "Content-Type": "application/json" },
132118
- body: JSON.stringify({ device_auth_id: deviceAuthId, user_code: userCode })
132335
+ body: JSON.stringify({ device_auth_id: deviceAuthId, user_code: userCode }),
132336
+ ...state.signal ? { signal: state.signal } : {}
132119
132337
  });
132120
132338
  if (res.ok) {
132121
132339
  const data = await res.json();
@@ -132125,7 +132343,8 @@ function openaiDeviceFlow(opts) {
132125
132343
  code: data.authorization_code,
132126
132344
  redirectUri: exchangeRedirectUri,
132127
132345
  clientId,
132128
- codeVerifier: data.code_verifier
132346
+ codeVerifier: data.code_verifier,
132347
+ ...state.signal ? { signal: state.signal } : {}
132129
132348
  })
132130
132349
  };
132131
132350
  }
@@ -132547,9 +132766,16 @@ async function* consumeResponsesSse(body, signal, emitReasoning = false) {
132547
132766
  if (errored)
132548
132767
  return;
132549
132768
  for (const entry of pending2.values()) {
132769
+ const outId = entry.callId || entry.id;
132770
+ if (!entry.emittedStart && entry.name) {
132771
+ entry.emittedStart = true;
132772
+ yield { type: "tool_use_start", id: outId, name: entry.name };
132773
+ }
132550
132774
  if (entry.emittedStart) {
132551
132775
  sawToolCall = true;
132552
- yield { type: "tool_use_end", id: entry.callId || entry.id, input: parseToolArgs(entry.args) };
132776
+ yield { type: "tool_use_end", id: outId, input: parseToolArgs(entry.args) };
132777
+ } else if (process.env.MOXXY_DEBUG) {
132778
+ console.error(`[openai-codex] dropping a truncated function_call with no name (id=${outId}, args=${entry.args.length}B)`);
132553
132779
  }
132554
132780
  }
132555
132781
  if (stopReason === "end_turn" && sawToolCall) {
@@ -132629,6 +132855,14 @@ var CodexProvider = class {
132629
132855
  yield toErrorEvent(err);
132630
132856
  return;
132631
132857
  }
132858
+ if (response.status === 401) {
132859
+ yield {
132860
+ type: "error",
132861
+ message: "ChatGPT OAuth credentials were rejected after a token refresh. Run `moxxy login openai-codex` to re-authenticate.",
132862
+ retryable: false
132863
+ };
132864
+ return;
132865
+ }
132632
132866
  }
132633
132867
  if (!response.ok || !response.body) {
132634
132868
  const text = await response.text().catch(() => "");
@@ -133253,9 +133487,6 @@ var localPlugin = definePlugin({
133253
133487
  version: "0.0.0",
133254
133488
  providers: [localProviderDef]
133255
133489
  });
133256
-
133257
- // ../plugin-stt-whisper/dist/audio.js
133258
- var MOXXY_PCM16_24KHZ_MIME = "audio/x-moxxy-pcm16-24khz";
133259
133490
  var WHISPER_FILENAME_BY_MIME = {
133260
133491
  "audio/ogg": "audio.ogg",
133261
133492
  "audio/opus": "audio.opus",
@@ -134823,6 +135054,18 @@ async function* runGoalMode(ctx) {
134823
135054
  continue;
134824
135055
  }
134825
135056
  reactiveCompactions = 0;
135057
+ if (reasoning) {
135058
+ yield await ctx.emit({
135059
+ type: "reasoning_message",
135060
+ sessionId: ctx.sessionId,
135061
+ turnId: ctx.turnId,
135062
+ source: "model",
135063
+ content: reasoning.text,
135064
+ ...reasoning.signature ? { signature: reasoning.signature } : {},
135065
+ ...reasoning.redacted ? { redacted: true } : {},
135066
+ ...reasoning.encrypted ? { encrypted: reasoning.encrypted } : {}
135067
+ });
135068
+ }
134826
135069
  if (totalTokens > GOAL_TOKEN_BUDGET) {
134827
135070
  yield await ctx.emit({
134828
135071
  type: "plugin_event",
@@ -134843,18 +135086,6 @@ async function* runGoalMode(ctx) {
134843
135086
  });
134844
135087
  return;
134845
135088
  }
134846
- if (reasoning) {
134847
- yield await ctx.emit({
134848
- type: "reasoning_message",
134849
- sessionId: ctx.sessionId,
134850
- turnId: ctx.turnId,
134851
- source: "model",
134852
- content: reasoning.text,
134853
- ...reasoning.signature ? { signature: reasoning.signature } : {},
134854
- ...reasoning.redacted ? { redacted: true } : {},
134855
- ...reasoning.encrypted ? { encrypted: reasoning.encrypted } : {}
134856
- });
134857
- }
134858
135089
  const stuck = yield* emitRequestsAndDetectStuck(ctx, toolUses, detector, {
134859
135090
  abortedResultMessage: "goal mode aborted (stuck pattern) before this call ran",
134860
135091
  nearHint: "against the same target (only volatile args varied)",
@@ -135455,6 +135686,16 @@ async function* runDeepResearchMode(ctx) {
135455
135686
  const synthesisText = await collectSynthesis(ctx, synthesisInput);
135456
135687
  if (synthesisText === null)
135457
135688
  return;
135689
+ if (ctx.signal.aborted) {
135690
+ yield await ctx.emit({
135691
+ type: "abort",
135692
+ sessionId: ctx.sessionId,
135693
+ turnId: ctx.turnId,
135694
+ source: "system",
135695
+ reason: "aborted during synthesis"
135696
+ });
135697
+ return;
135698
+ }
135458
135699
  yield await ctx.emit({
135459
135700
  type: "assistant_message",
135460
135701
  sessionId: ctx.sessionId,
@@ -135907,11 +136148,10 @@ async function createUnixSocketServer(socketPath, logger = stderrLogger) {
135907
136148
  } else {
135908
136149
  warnWindowsPipeAclOnce(logger, socketPath);
135909
136150
  }
135910
- const connectionHandlers = [];
136151
+ let connectionHandler;
135911
136152
  const server = net2.createServer((socket) => {
135912
136153
  const transport = new NdjsonTransport(socket);
135913
- for (const handler of connectionHandlers)
135914
- handler(transport);
136154
+ connectionHandler?.(transport);
135915
136155
  });
135916
136156
  await new Promise((resolve12, reject) => {
135917
136157
  server.once("error", reject);
@@ -135933,7 +136173,7 @@ async function createUnixSocketServer(socketPath, logger = stderrLogger) {
135933
136173
  return {
135934
136174
  address: socketPath,
135935
136175
  onConnection(handler) {
135936
- connectionHandlers.push(handler);
136176
+ connectionHandler = handler;
135937
136177
  },
135938
136178
  close() {
135939
136179
  return new Promise((resolve12) => {
@@ -138842,6 +139082,57 @@ async function integrate(input) {
138842
139082
  }
138843
139083
  return { merged, conflicts, resolvedByOwnership, stagingBranch: branchName, promoted };
138844
139084
  }
139085
+ var COLLAB_LOCK_PATH = join(homedir(), ".moxxy", "collab", "active.lock");
139086
+ function collabLockPath() {
139087
+ return process.env.MOXXY_COLLAB_LOCK || COLLAB_LOCK_PATH;
139088
+ }
139089
+ function readRaw() {
139090
+ try {
139091
+ return JSON.parse(readFileSync(collabLockPath(), "utf8"));
139092
+ } catch {
139093
+ return null;
139094
+ }
139095
+ }
139096
+ function isAlive(pid) {
139097
+ try {
139098
+ process.kill(pid, 0);
139099
+ return true;
139100
+ } catch (err) {
139101
+ return err.code === "EPERM";
139102
+ }
139103
+ }
139104
+ function readActiveCollab() {
139105
+ const info = readRaw();
139106
+ if (!info)
139107
+ return null;
139108
+ if (!isAlive(info.pid)) {
139109
+ try {
139110
+ unlinkSync(collabLockPath());
139111
+ } catch {
139112
+ }
139113
+ return null;
139114
+ }
139115
+ return info;
139116
+ }
139117
+ function tryAcquireCollabLock(args) {
139118
+ mkdirSync(dirname(collabLockPath()), { recursive: true });
139119
+ const existing = readActiveCollab();
139120
+ if (existing && existing.sessionId !== args.sessionId) {
139121
+ return { ok: false, holder: existing };
139122
+ }
139123
+ const info = { pid: process.pid, ...args };
139124
+ writeFileSync(collabLockPath(), JSON.stringify(info));
139125
+ return { ok: true };
139126
+ }
139127
+ function releaseCollabLock(sessionId) {
139128
+ const info = readRaw();
139129
+ if (info && info.sessionId === sessionId) {
139130
+ try {
139131
+ unlinkSync(collabLockPath());
139132
+ } catch {
139133
+ }
139134
+ }
139135
+ }
138845
139136
 
138846
139137
  // ../mode-collaborative/dist/collab-loop.js
138847
139138
  var POLL_MS = 500;
@@ -138860,47 +139151,56 @@ async function* runCollaborative(ctx, deps) {
138860
139151
  yield await ctx.emit(assistant(ctx, "Collaborative mode needs a task to work on."));
138861
139152
  return;
138862
139153
  }
138863
- const runId = collabRunId(String(ctx.sessionId), String(ctx.turnId));
138864
- mkdirSync(collabRunDir(runId), { recursive: true });
138865
- const { installed: gitInstalled2, repo: gitRepo } = await detectGit(cwd2);
138866
- const parallel = cfg.concurrency === "parallel" && gitRepo;
138867
- if (!parallel) {
138868
- yield await ctx.emit(plugin4(ctx, "collab_fallback_sequential", {
138869
- reason: !gitInstalled2 ? "git is not installed \u2014 running agents sequentially in your workspace" : !gitRepo ? "this folder is not a git repository \u2014 running agents sequentially in your workspace" : "sequential mode selected"
138870
- }));
138871
- }
138872
- let baseSha = "";
138873
- if (parallel) {
138874
- const base2 = await resolveBase(cwd2);
138875
- baseSha = base2.baseSha;
139154
+ const lock = tryAcquireCollabLock({ sessionId: String(ctx.sessionId), task, startedAtMs: Date.now() });
139155
+ if (!lock.ok) {
139156
+ yield await ctx.emit(plugin4(ctx, "collab_blocked", { reason: "already-running", holderTask: lock.holder.task }));
139157
+ yield await ctx.emit(assistant(ctx, `A collaboration is already running ("${lock.holder.task}"). Only one runs at a time to save resources \u2014 stop it first, then start again.`));
139158
+ return;
138876
139159
  }
139160
+ const runId = collabRunId(String(ctx.sessionId), String(ctx.turnId));
138877
139161
  const worktrees = /* @__PURE__ */ new Map();
138878
- const architectEntry = {
138879
- id: ARCHITECT_AGENT_ID,
138880
- name: "Architect",
138881
- role: "architect",
138882
- subtask: task
138883
- };
138884
- const hub = await createCollaborationHub({
138885
- socketPath: hubSocketPath(runId),
138886
- task,
138887
- roster: [architectEntry],
138888
- peerReader: peerReaderFor(worktrees, baseSha)
138889
- });
138890
- registerActiveHub(String(ctx.sessionId), hub);
138891
- const unsubscribe = hub.subscribe((e3) => {
138892
- void ctx.emit(toCollabEvent(ctx, e3));
138893
- });
138894
- const supervisorOpts = {
138895
- runId,
138896
- hubSocket: hub.socketPath,
138897
- coordinatorSessionId: String(ctx.sessionId),
138898
- parentTask: task,
138899
- ...cfg.defaultModel ? { defaultModel: cfg.defaultModel } : {},
138900
- signal: ctx.signal
138901
- };
138902
- const supervisor = (deps.createSupervisor ?? ((o2) => new PeerSupervisor(o2)))(supervisorOpts, hub);
139162
+ let hub = null;
139163
+ let supervisor = null;
139164
+ let unsubscribe = null;
138903
139165
  try {
139166
+ mkdirSync(collabRunDir(runId), { recursive: true });
139167
+ const { installed: gitInstalled2, repo: gitRepo } = await detectGit(cwd2);
139168
+ const parallel = cfg.concurrency === "parallel" && gitRepo;
139169
+ if (!parallel) {
139170
+ yield await ctx.emit(plugin4(ctx, "collab_fallback_sequential", {
139171
+ reason: !gitInstalled2 ? "git is not installed \u2014 running agents sequentially in your workspace" : !gitRepo ? "this folder is not a git repository \u2014 running agents sequentially in your workspace" : "sequential mode selected"
139172
+ }));
139173
+ }
139174
+ let baseSha = "";
139175
+ if (parallel) {
139176
+ const base2 = await resolveBase(cwd2, { snapshotDirty: true });
139177
+ baseSha = base2.baseSha;
139178
+ }
139179
+ const architectEntry = {
139180
+ id: ARCHITECT_AGENT_ID,
139181
+ name: "Architect",
139182
+ role: "architect",
139183
+ subtask: task
139184
+ };
139185
+ hub = await createCollaborationHub({
139186
+ socketPath: hubSocketPath(runId),
139187
+ task,
139188
+ roster: [architectEntry],
139189
+ peerReader: peerReaderFor(worktrees, baseSha)
139190
+ });
139191
+ registerActiveHub(String(ctx.sessionId), hub);
139192
+ unsubscribe = hub.subscribe((e3) => {
139193
+ void ctx.emit(toCollabEvent(ctx, e3));
139194
+ });
139195
+ const supervisorOpts = {
139196
+ runId,
139197
+ hubSocket: hub.socketPath,
139198
+ coordinatorSessionId: String(ctx.sessionId),
139199
+ parentTask: task,
139200
+ ...cfg.defaultModel ? { defaultModel: cfg.defaultModel } : {},
139201
+ signal: ctx.signal
139202
+ };
139203
+ supervisor = (deps.createSupervisor ?? ((o2) => new PeerSupervisor(o2)))(supervisorOpts, hub);
138904
139204
  yield await ctx.emit(plugin4(ctx, "collab_started", { task, parallel, gitInstalled: gitInstalled2, gitRepo }));
138905
139205
  supervisor.spawn({ entry: architectEntry, cwd: cwd2, mode: COLLAB_ARCHITECT_MODE_NAME });
138906
139206
  yield await ctx.emit(plugin4(ctx, "collab_agent_spawned", { id: ARCHITECT_AGENT_ID, role: "architect" }));
@@ -138998,10 +139298,14 @@ ${summaryBlock}${mergeNote ? `
138998
139298
  ${mergeNote}` : ""}`));
138999
139299
  yield await ctx.emit(plugin4(ctx, "collab_completed", { done: doneIds, total: roster.length }));
139000
139300
  } finally {
139001
- await supervisor.shutdownAll("collaboration complete");
139002
- unsubscribe();
139301
+ if (supervisor)
139302
+ await supervisor.shutdownAll("collaboration complete");
139303
+ if (unsubscribe)
139304
+ unsubscribe();
139003
139305
  unregisterActiveHub(String(ctx.sessionId));
139004
- await hub.close();
139306
+ if (hub)
139307
+ await hub.close();
139308
+ releaseCollabLock(String(ctx.sessionId));
139005
139309
  }
139006
139310
  }
139007
139311
  function lastUserPromptText(ctx) {
@@ -139655,6 +139959,7 @@ var import_grammy6 = __toESM(require_mod2());
139655
139959
 
139656
139960
  // ../plugin-telegram/dist/channel.js
139657
139961
  var import_grammy5 = __toESM(require_mod2());
139962
+ init_dist();
139658
139963
 
139659
139964
  // ../plugin-telegram/dist/permission.js
139660
139965
  var TelegramPermissionResolver = class {
@@ -140111,7 +140416,16 @@ function composeFrame(snap) {
140111
140416
  return parts.join("\n\n");
140112
140417
  }
140113
140418
  function stripHtml(html) {
140114
- return html.replace(/<\/?[a-z][^>]*>/gi, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"');
140419
+ return html.replace(/<\/?[a-z][^>]*>/gi, "").replace(/&#(\d+);/g, (_2, dec) => safeFromCodePoint(parseInt(dec, 10))).replace(/&#x([0-9a-f]+);/gi, (_2, hex) => safeFromCodePoint(parseInt(hex, 16))).replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&amp;/g, "&");
140420
+ }
140421
+ function safeFromCodePoint(cp) {
140422
+ if (!Number.isFinite(cp) || cp < 0 || cp > 1114111)
140423
+ return "";
140424
+ try {
140425
+ return String.fromCodePoint(cp);
140426
+ } catch {
140427
+ return "";
140428
+ }
140115
140429
  }
140116
140430
  function truncate3(s2, n2) {
140117
140431
  return s2.length <= n2 ? s2 : s2.slice(0, n2) + "\u2026";
@@ -140882,13 +141196,11 @@ function mapChoice(choice) {
140882
141196
  }
140883
141197
 
140884
141198
  // ../plugin-telegram/dist/channel/turn-runner.js
140885
- init_dist();
140886
141199
  async function runUserTurn(ctx, deps, opts) {
140887
141200
  const { session, bot, framePump, typing, logger } = deps;
140888
- const { chatId, text, model, controller } = opts;
141201
+ const { chatId, text, model, controller, turnId } = opts;
140889
141202
  framePump.beginTurn(chatId);
140890
141203
  typing.start(bot, chatId);
140891
- const turnId = newTurnId();
140892
141204
  const unsubscribe = session.log.subscribe((event) => {
140893
141205
  if (event.turnId !== turnId)
140894
141206
  return;
@@ -141089,6 +141401,12 @@ var TelegramChannel = class {
141089
141401
  // without poisoning the session-level signal (which other channels
141090
141402
  // sharing the same Session would also observe).
141091
141403
  turnController = null;
141404
+ // turnIds of turns THIS channel initiated. mirrorForeignTurn filters on these
141405
+ // (invariant #8: filter event-log subscribers by turnId when multiplexing
141406
+ // turns on one Session) rather than the coarse `busy` flag alone — so an
141407
+ // assistant_message dispatched for our own turn AFTER `busy` flips false
141408
+ // (async event ordering / RemoteSession replay) isn't re-mirrored as foreign.
141409
+ ownTurnIds = /* @__PURE__ */ new Set();
141092
141410
  // When a user clicks an approval option that needs text follow-up
141093
141411
  // (e.g. plan-execute "Redraft with feedback"), we stash the
141094
141412
  // approval+option pair and capture the user's NEXT message as the
@@ -141234,6 +141552,13 @@ var TelegramChannel = class {
141234
141552
  const controller = new AbortController();
141235
141553
  this.turnController = controller;
141236
141554
  const effectiveModel = this.activeModelOverride ?? this.model;
141555
+ const turnId = newTurnId();
141556
+ this.ownTurnIds.add(turnId);
141557
+ if (this.ownTurnIds.size > 64) {
141558
+ const oldest = this.ownTurnIds.values().next().value;
141559
+ if (oldest !== void 0)
141560
+ this.ownTurnIds.delete(oldest);
141561
+ }
141237
141562
  try {
141238
141563
  await runUserTurn(ctx, {
141239
141564
  session: this.session,
@@ -141241,7 +141566,7 @@ var TelegramChannel = class {
141241
141566
  framePump: this.framePump,
141242
141567
  typing: this.typing,
141243
141568
  ...this.opts.logger ? { logger: this.opts.logger } : {}
141244
- }, { chatId, text, model: effectiveModel, controller });
141569
+ }, { chatId, text, model: effectiveModel, controller, turnId });
141245
141570
  } finally {
141246
141571
  this.busy = false;
141247
141572
  this.turnController = null;
@@ -141255,7 +141580,11 @@ var TelegramChannel = class {
141255
141580
  * avoid parse-mode pitfalls; the view itself lives on the web surface.
141256
141581
  */
141257
141582
  mirrorForeignTurn(event) {
141258
- if (event.type !== "assistant_message" || this.busy)
141583
+ if (event.type !== "assistant_message")
141584
+ return;
141585
+ if (this.ownTurnIds.has(event.turnId))
141586
+ return;
141587
+ if (this.busy)
141259
141588
  return;
141260
141589
  if (!this.bot || this.lastChatId == null)
141261
141590
  return;
@@ -142332,10 +142661,16 @@ async function runPairFlow(ctx) {
142332
142661
  await session.close("SIGINT").catch(() => void 0);
142333
142662
  process.exit(0);
142334
142663
  };
142335
- process.on("SIGINT", () => void shutdown());
142336
- process.on("SIGTERM", () => void shutdown());
142337
- await handle2.running;
142338
- return 0;
142664
+ const onSignal = () => void shutdown();
142665
+ process.once("SIGINT", onSignal);
142666
+ process.once("SIGTERM", onSignal);
142667
+ try {
142668
+ await handle2.running;
142669
+ return 0;
142670
+ } finally {
142671
+ process.removeListener("SIGINT", onSignal);
142672
+ process.removeListener("SIGTERM", onSignal);
142673
+ }
142339
142674
  }
142340
142675
 
142341
142676
  // ../plugin-telegram/dist/setup-wizard.js
@@ -142582,8 +142917,8 @@ function buildTelegramPlugin(opts) {
142582
142917
  if (!targetChat) {
142583
142918
  throw new Error("no authorized chat \u2014 run `moxxy channels telegram pair` first or pass `chatId` explicitly");
142584
142919
  }
142585
- const bot = new import_grammy6.Bot(token);
142586
- await bot.api.sendMessage(targetChat, text, parseMode ? { parse_mode: parseMode } : {});
142920
+ const api = new import_grammy6.Api(token);
142921
+ await api.sendMessage(targetChat, text, parseMode ? { parse_mode: parseMode } : {});
142587
142922
  return { delivered: true, chatId: targetChat, length: text.length };
142588
142923
  }
142589
142924
  }),
@@ -142816,6 +143151,9 @@ var HttpChannel = class {
142816
143151
  this.permissionResolver = opts.allowedTools && opts.allowedTools.length > 0 ? createAllowListResolver([...opts.allowedTools]) : denyByDefaultResolver;
142817
143152
  }
142818
143153
  async start(startOpts) {
143154
+ if (this.server) {
143155
+ throw new Error("HttpChannel is already started \u2014 call stop() before starting again.");
143156
+ }
142819
143157
  const ctx = {
142820
143158
  session: startOpts.session,
142821
143159
  authToken: this.authToken,
@@ -142874,11 +143212,16 @@ var HttpChannel = class {
142874
143212
  return {
142875
143213
  running,
142876
143214
  stop: async () => {
143215
+ const srv = this.server;
142877
143216
  await new Promise((resolve12) => {
142878
- if (!this.server)
143217
+ if (!srv)
142879
143218
  return resolve12();
142880
- this.server.close(() => resolve12());
143219
+ srv.close(() => resolve12());
142881
143220
  });
143221
+ if (this.server === srv) {
143222
+ this.server = null;
143223
+ this.boundPortValue = 0;
143224
+ }
142882
143225
  }
142883
143226
  };
142884
143227
  }
@@ -143052,13 +143395,19 @@ async function pidCommand2(pid) {
143052
143395
  function looksLikeMoxxy(command) {
143053
143396
  return command.length > 0 && /moxxy/i.test(command);
143054
143397
  }
143055
- async function freeTcpPortIfMoxxy(port, logger) {
143398
+ var realFreePortDeps = {
143399
+ pidsListeningOn,
143400
+ pidCommand: pidCommand2,
143401
+ kill: (pid, signal) => process.kill(pid, signal),
143402
+ graceMs: 400
143403
+ };
143404
+ async function freeTcpPortIfMoxxy(port, logger, deps = realFreePortDeps) {
143056
143405
  if (process.platform === "win32")
143057
143406
  return false;
143058
- const pids = (await pidsListeningOn(port)).filter((pid) => pid !== process.pid);
143407
+ const pids = (await deps.pidsListeningOn(port)).filter((pid) => pid !== process.pid);
143059
143408
  if (pids.length === 0)
143060
143409
  return false;
143061
- const holders = await Promise.all(pids.map(async (pid) => ({ pid, command: await pidCommand2(pid) })));
143410
+ const holders = await Promise.all(pids.map(async (pid) => ({ pid, command: await deps.pidCommand(pid) })));
143062
143411
  const foreign = holders.filter((h3) => !looksLikeMoxxy(h3.command));
143063
143412
  if (foreign.length > 0) {
143064
143413
  logger?.warn?.(`port ${port} is held by non-moxxy process(es); not killing them`, {
@@ -143066,17 +143415,29 @@ async function freeTcpPortIfMoxxy(port, logger) {
143066
143415
  });
143067
143416
  return false;
143068
143417
  }
143418
+ let attempted = false;
143069
143419
  for (const { pid } of holders) {
143420
+ if (!looksLikeMoxxy(await deps.pidCommand(pid)))
143421
+ continue;
143070
143422
  try {
143071
- process.kill(pid, "SIGTERM");
143423
+ deps.kill(pid, "SIGTERM");
143424
+ attempted = true;
143072
143425
  } catch {
143073
143426
  }
143074
143427
  }
143075
- await new Promise((r2) => setTimeout(r2, 400));
143428
+ if (!attempted)
143429
+ return false;
143430
+ await new Promise((r2) => setTimeout(r2, deps.graceMs ?? 400));
143076
143431
  for (const { pid } of holders) {
143077
143432
  try {
143078
- process.kill(pid, 0);
143079
- process.kill(pid, "SIGKILL");
143433
+ deps.kill(pid, 0);
143434
+ } catch {
143435
+ continue;
143436
+ }
143437
+ if (!looksLikeMoxxy(await deps.pidCommand(pid)))
143438
+ continue;
143439
+ try {
143440
+ deps.kill(pid, "SIGKILL");
143080
143441
  } catch {
143081
143442
  }
143082
143443
  }
@@ -143634,6 +143995,7 @@ async function createWebSocketTransportServer(opts) {
143634
143995
  let currentToken = opts.authToken;
143635
143996
  let currentAllowedOrigins = opts.allowedOrigins ?? [];
143636
143997
  let connections = 0;
143998
+ let pending2 = 0;
143637
143999
  const wss = new import_websocket_server.default({
143638
144000
  host,
143639
144001
  port: opts.port,
@@ -143643,11 +144005,14 @@ async function createWebSocketTransportServer(opts) {
143643
144005
  console.warn(`[moxxy] ws bridge: rejected browser-origin upgrade (Origin: ${String(info.req.headers.origin)})`);
143644
144006
  return false;
143645
144007
  }
143646
- if (connections >= maxConnections) {
144008
+ if (connections + pending2 >= maxConnections) {
143647
144009
  console.warn(`[moxxy] ws bridge: rejected upgrade \u2014 connection cap (${maxConnections}) reached`);
143648
144010
  return false;
143649
144011
  }
143650
- return checkWsAuth(info.req, currentToken, { allowQueryToken: opts.allowQueryToken });
144012
+ const ok = checkWsAuth(info.req, currentToken, { allowQueryToken: opts.allowQueryToken });
144013
+ if (ok)
144014
+ pending2 += 1;
144015
+ return ok;
143651
144016
  },
143652
144017
  // When the client offers subprotocols (the moxxy.bearer.* convention),
143653
144018
  // select the moxxy protocol WITHOUT echoing the token-bearing entry back.
@@ -143655,6 +144020,8 @@ async function createWebSocketTransportServer(opts) {
143655
144020
  });
143656
144021
  const connectionHandlers = [];
143657
144022
  wss.on("connection", (ws) => {
144023
+ if (pending2 > 0)
144024
+ pending2 -= 1;
143658
144025
  connections += 1;
143659
144026
  ws.once("close", () => {
143660
144027
  connections -= 1;
@@ -143762,6 +144129,7 @@ var optionalWorkspace = z.string().min(1).max(256).optional();
143762
144129
  var MAX_AUDIO_BASE64 = 4e7;
143763
144130
  var MAX_INLINE_ATTACHMENT_CONTENT = 12e6;
143764
144131
  var commandName = z.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "invalid command name");
144132
+ var appId = z.string().min(1).max(64).regex(/^[a-z][a-z0-9-]*$/, "invalid app id");
143765
144133
  var workflowName = z.string().min(1).max(200).refine((s2) => !s2.includes("..") && !s2.includes("/") && !s2.includes("\\"), "invalid workflow name");
143766
144134
  var ipcInputSchemas = {
143767
144135
  // No-arg, but spawns a child process (npm install) — pin the payload to
@@ -143871,6 +144239,20 @@ var ipcInputSchemas = {
143871
144239
  "settings.writeSkill": z.object({ name: skillName, body: z.string().max(1e6) }),
143872
144240
  "settings.readSkill": z.object({ name: skillName }),
143873
144241
  "settings.deleteSkill": z.object({ name: skillName }),
144242
+ // Desktop apps: appId keys the per-app install dir + a network download, so
144243
+ // pin it to a non-traversing slug. (pickDocument is no-arg → see below.)
144244
+ "apps.status": z.object({ appId }),
144245
+ "apps.install": z.object({ appId }),
144246
+ "apps.uninstall": z.object({ appId }),
144247
+ // Anonymizer: parseDocument reads a file (bound the path), saveRedacted writes
144248
+ // one (bound name + cap content so a renderer can't OOM main). pickDocument
144249
+ // takes nothing — pin it to "nothing" so no args can be smuggled across.
144250
+ "anonymizer.pickDocument": z.undefined(),
144251
+ "anonymizer.parseDocument": z.object({ path: z.string().min(1).max(4096) }),
144252
+ "anonymizer.saveRedacted": z.object({
144253
+ suggestedName: z.string().min(1).max(255),
144254
+ content: z.string().max(2e7)
144255
+ }),
143874
144256
  "desks.create": z.object({ name: z.string().min(1).max(200), cwd: z.string().min(1).max(4096) }),
143875
144257
  // Mirror desks.create's name bounds — rename writes the name into the desks
143876
144258
  // JSON, so an unbounded string would let a renderer bloat the state file.
@@ -144105,6 +144487,7 @@ var MobileSessionHost = class {
144105
144487
  this.bus.handle("session.newSession", async () => {
144106
144488
  for (const controller of this.turns.values())
144107
144489
  controller.abort();
144490
+ this.autoApprove = false;
144108
144491
  if (this.session.reset)
144109
144492
  await this.session.reset();
144110
144493
  else
@@ -144251,6 +144634,8 @@ var MobileSessionHost = class {
144251
144634
  return { turnId };
144252
144635
  }
144253
144636
  openAsk(req) {
144637
+ if (this.disposed)
144638
+ return Promise.resolve({ mode: "deny" });
144254
144639
  const requestId = `ask-${++this.askCounter}`;
144255
144640
  return new Promise((resolve12) => {
144256
144641
  this.pendingAsks.set(requestId, resolve12);
@@ -144562,7 +144947,7 @@ function htmlToMarkdown(html, opts = {}) {
144562
144947
  ${"#".repeat(Number(lvl))} ${stripTags(inner).trim()}
144563
144948
 
144564
144949
  `);
144565
- body = body.replace(/<a\b[^>]*\bhref="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi, (_2, href, inner) => `[${stripTags(inner).trim()}](${href})`);
144950
+ body = body.replace(/<a\b[^>]*\bhref=(?:"([^"]+)"|'([^']+)'|([^\s>]+))[^>]*>([\s\S]*?)<\/a>/gi, (_2, dq, sq, uq, inner) => `[${stripTags(inner).trim()}](${dq ?? sq ?? uq})`);
144566
144951
  body = body.replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, (_2, inner) => `
144567
144952
 
144568
144953
  \`\`\`
@@ -145479,6 +145864,8 @@ var TerminalProcessImpl = class {
145479
145864
  child.stderr.on("data", (b3) => this.emitData(b3.toString("utf8")));
145480
145865
  child.on("exit", (code) => this.emitExit(code ?? 0));
145481
145866
  child.on("error", () => this.emitExit(1));
145867
+ child.stdin.on("error", () => {
145868
+ });
145482
145869
  }
145483
145870
  }
145484
145871
  emitData(d2) {
@@ -145518,10 +145905,13 @@ var TerminalProcessImpl = class {
145518
145905
  write(data) {
145519
145906
  if (!this.alive)
145520
145907
  return;
145521
- if (this.pty)
145522
- this.pty.write(data);
145523
- else
145524
- this.child?.stdin.write(data);
145908
+ try {
145909
+ if (this.pty)
145910
+ this.pty.write(data);
145911
+ else
145912
+ this.child?.stdin.write(data);
145913
+ } catch {
145914
+ }
145525
145915
  }
145526
145916
  resize(cols, rows) {
145527
145917
  if (this.pty && this.alive) {
@@ -146137,16 +146527,16 @@ function buildProviderAdminPlugin(opts) {
146137
146527
  // ../plugin-usage-stats/dist/index.js
146138
146528
  init_dist();
146139
146529
  function buildUsageStatsPlugin(opts = {}) {
146140
- let cursor = 0;
146530
+ let initMaxSeq = null;
146141
146531
  return definePlugin({
146142
146532
  name: "@moxxy/plugin-usage-stats",
146143
146533
  version: "0.0.0",
146144
146534
  hooks: {
146145
146535
  onInit(ctx) {
146146
- cursor = ctx.log.length;
146536
+ initMaxSeq = maxSeq(ctx.log.slice());
146147
146537
  },
146148
146538
  async onShutdown(ctx) {
146149
- const live = ctx.log.slice(cursor);
146539
+ const live = initMaxSeq === null ? ctx.log.slice() : ctx.log.slice().filter((e3) => e3.seq > initMaxSeq);
146150
146540
  if (live.length === 0)
146151
146541
  return;
146152
146542
  const delta = summarizeTokensByModel(live);
@@ -146155,6 +146545,13 @@ function buildUsageStatsPlugin(opts = {}) {
146155
146545
  }
146156
146546
  });
146157
146547
  }
146548
+ function maxSeq(events) {
146549
+ let max = null;
146550
+ for (const e3 of events)
146551
+ if (max === null || e3.seq > max)
146552
+ max = e3.seq;
146553
+ return max;
146554
+ }
146158
146555
  var infoCmd = {
146159
146556
  name: "info",
146160
146557
  description: "Show provider \xB7 model \xB7 mode \xB7 plugin/skill counts",
@@ -146247,36 +146644,25 @@ async function compactSession(session) {
146247
146644
  if (events.length === 0) {
146248
146645
  return { kind: "text", text: "nothing to compact: event log is empty" };
146249
146646
  }
146250
- const providerCtxWindow = resolveActiveContextWindow(s2);
146251
146647
  const provider = safe(() => s2.providers?.getActive()) ?? void 0;
146252
146648
  const model = provider?.models[0]?.id;
146649
+ const contextWindow = provider?.models[0]?.contextWindow;
146253
146650
  try {
146254
- const result = await compactor.compact(events, {
146255
- log: s2.log.asReader ? s2.log.asReader() : s2.log,
146256
- budget: {
146257
- contextWindow: providerCtxWindow,
146258
- estimatedTokens: estimateContextTokens$1(s2.log.asReader ? s2.log.asReader() : s2.log),
146259
- reserveForOutput: 0
146260
- },
146261
- signal: s2.signal ?? new AbortController().signal,
146651
+ const result = await runManualCompaction({
146652
+ compactor,
146653
+ log: s2.log,
146654
+ signal: s2.signal,
146262
146655
  ...provider ? { provider } : {},
146263
- ...model ? { model } : {}
146656
+ ...model !== void 0 ? { model } : {},
146657
+ ...contextWindow !== void 0 ? { contextWindow } : {},
146658
+ ...s2.id !== void 0 ? { sessionId: s2.id } : {}
146264
146659
  });
146265
- if (result.tokensSaved <= 0 || result.summary.trim().length === 0) {
146660
+ if (!result.compacted) {
146266
146661
  return { kind: "text", text: "nothing to compact yet" };
146267
146662
  }
146268
- const lastEvent = events[events.length - 1];
146269
- const emittable = {
146270
- sessionId: s2.id ?? lastEvent?.sessionId,
146271
- turnId: lastEvent?.turnId,
146272
- source: "compactor",
146273
- ...result
146274
- };
146275
- await s2.log.append(emittable);
146276
- const compactedEvents = result.replacedRange[1] - result.replacedRange[0] + 1;
146277
146663
  return {
146278
146664
  kind: "text",
146279
- text: `context compacted: ${formatCount2(compactedEvents)} ${plural2(compactedEvents, "event")}, ~${formatTokenCount2(result.tokensSaved)} tokens saved`
146665
+ text: `context compacted: ${formatCount2(result.eventsCompacted)} ${plural2(result.eventsCompacted, "event")}, ~${formatTokenCount2(result.tokensSaved)} tokens saved`
146280
146666
  };
146281
146667
  } catch (err) {
146282
146668
  return {
@@ -146285,17 +146671,6 @@ async function compactSession(session) {
146285
146671
  };
146286
146672
  }
146287
146673
  }
146288
- function resolveActiveContextWindow(s2) {
146289
- try {
146290
- const provider = s2.providers?.getActive();
146291
- if (!provider)
146292
- return Number.MAX_SAFE_INTEGER;
146293
- const window2 = provider.models[0]?.contextWindow;
146294
- return window2 && window2 > 0 ? window2 : Number.MAX_SAFE_INTEGER;
146295
- } catch {
146296
- return Number.MAX_SAFE_INTEGER;
146297
- }
146298
- }
146299
146674
  function formatCount2(value) {
146300
146675
  return new Intl.NumberFormat("en-US").format(value);
146301
146676
  }
@@ -146348,15 +146723,26 @@ function buildViewPlugin(opts) {
146348
146723
  });
146349
146724
  }
146350
146725
  var IS_DARWIN = process.platform === "darwin";
146726
+ function procFailureCause(proc, timeoutMs) {
146727
+ if (proc.timedOut) {
146728
+ return timeoutMs ? `timed out after ${timeoutMs}ms` : "timed out";
146729
+ }
146730
+ if (proc.aborted)
146731
+ return "aborted (turn cancelled)";
146732
+ return "";
146733
+ }
146351
146734
  function runProcess2(cmd, args, opts = {}) {
146352
146735
  return new Promise((resolve12, reject) => {
146353
146736
  const child = spawn(cmd, [...args], { stdio: ["pipe", "pipe", "pipe"] });
146354
146737
  const stdoutChunks = [];
146355
146738
  let stderr = "";
146356
146739
  let settled = false;
146740
+ let timedOut = false;
146741
+ let aborted = false;
146357
146742
  const onAbort = () => {
146358
146743
  if (settled)
146359
146744
  return;
146745
+ aborted = true;
146360
146746
  try {
146361
146747
  child.kill("SIGTERM");
146362
146748
  } catch {
@@ -146366,6 +146752,7 @@ function runProcess2(cmd, args, opts = {}) {
146366
146752
  const timer = opts.timeoutMs ? setTimeout(() => {
146367
146753
  if (settled)
146368
146754
  return;
146755
+ timedOut = true;
146369
146756
  try {
146370
146757
  child.kill("SIGTERM");
146371
146758
  } catch {
@@ -146396,7 +146783,9 @@ function runProcess2(cmd, args, opts = {}) {
146396
146783
  resolve12({
146397
146784
  exitCode: code ?? -1,
146398
146785
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
146399
- stderr
146786
+ stderr,
146787
+ timedOut,
146788
+ aborted
146400
146789
  });
146401
146790
  });
146402
146791
  if (opts.input !== void 0) {
@@ -146429,10 +146818,11 @@ var applescriptTool = defineTool({
146429
146818
  timeoutMs: 3e4
146430
146819
  });
146431
146820
  if (proc.exitCode !== 0) {
146821
+ const cause = procFailureCause(proc, 3e4);
146432
146822
  throw new MoxxyError({
146433
146823
  code: "TOOL_ERROR",
146434
- message: `osascript failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(no error message)"}`,
146435
- context: { tool: "computer_applescript", exitCode: proc.exitCode }
146824
+ message: cause ? `osascript ${cause}` : `osascript failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(no error message)"}`,
146825
+ context: { tool: "computer_applescript", exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 }
146436
146826
  });
146437
146827
  }
146438
146828
  return { ok: true, output: proc.stdout.trim() };
@@ -146460,10 +146850,11 @@ end tell`;
146460
146850
  timeoutMs: 1e4
146461
146851
  });
146462
146852
  if (proc.exitCode !== 0) {
146853
+ const cause = procFailureCause(proc, 1e4);
146463
146854
  throw new MoxxyError({
146464
146855
  code: "TOOL_ERROR",
146465
- message: `click failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(check Accessibility permission in System Settings \u2192 Privacy & Security \u2192 Accessibility)"}`,
146466
- context: { tool: "computer_click", exitCode: proc.exitCode }
146856
+ message: cause ? `click ${cause}` : `click failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(check Accessibility permission in System Settings \u2192 Privacy & Security \u2192 Accessibility)"}`,
146857
+ context: { tool: "computer_click", exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 }
146467
146858
  });
146468
146859
  }
146469
146860
  return { ok: true, x: x4, y: y2, count: n2 };
@@ -146485,10 +146876,11 @@ var clipboardTool = defineTool({
146485
146876
  timeoutMs: 5e3
146486
146877
  });
146487
146878
  if (proc2.exitCode !== 0) {
146879
+ const cause = procFailureCause(proc2, 5e3);
146488
146880
  throw new MoxxyError({
146489
146881
  code: "TOOL_ERROR",
146490
- message: `pbpaste failed (exit ${proc2.exitCode}): ${proc2.stderr.trim()}`,
146491
- context: { tool: "computer_clipboard", exitCode: proc2.exitCode }
146882
+ message: cause ? `pbpaste ${cause}` : `pbpaste failed (exit ${proc2.exitCode}): ${proc2.stderr.trim()}`,
146883
+ context: { tool: "computer_clipboard", exitCode: proc2.exitCode, timedOut: proc2.timedOut ? 1 : 0 }
146492
146884
  });
146493
146885
  }
146494
146886
  return { ok: true, text: proc2.stdout };
@@ -146506,10 +146898,11 @@ var clipboardTool = defineTool({
146506
146898
  timeoutMs: 5e3
146507
146899
  });
146508
146900
  if (proc.exitCode !== 0) {
146901
+ const cause = procFailureCause(proc, 5e3);
146509
146902
  throw new MoxxyError({
146510
146903
  code: "TOOL_ERROR",
146511
- message: `pbcopy failed (exit ${proc.exitCode}): ${proc.stderr.trim()}`,
146512
- context: { tool: "computer_clipboard", exitCode: proc.exitCode }
146904
+ message: cause ? `pbcopy ${cause}` : `pbcopy failed (exit ${proc.exitCode}): ${proc.stderr.trim()}`,
146905
+ context: { tool: "computer_clipboard", exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 }
146513
146906
  });
146514
146907
  }
146515
146908
  return { ok: true, length: text.length };
@@ -146547,6 +146940,16 @@ var KEY_CODES = {
146547
146940
  f11: 103,
146548
146941
  f12: 111
146549
146942
  };
146943
+ var WHITESPACE_KEY_CODES = {
146944
+ " ": 49,
146945
+ // space
146946
+ " ": 48,
146947
+ // tab
146948
+ "\r": 36,
146949
+ // return
146950
+ "\n": 36
146951
+ // return
146952
+ };
146550
146953
  var keyTool = defineTool({
146551
146954
  name: "computer_key",
146552
146955
  description: "Send a single key chord with optional modifiers. Use this for shortcuts (cmd+c, cmd+tab, cmd+shift+4) and named keys (return, tab, escape, arrows, page_up/down, f1\u2013f12). For typing arbitrary text, use computer_type.",
@@ -146564,10 +146967,11 @@ var keyTool = defineTool({
146564
146967
  timeoutMs: 1e4
146565
146968
  });
146566
146969
  if (proc.exitCode !== 0) {
146970
+ const cause = procFailureCause(proc, 1e4);
146567
146971
  throw new MoxxyError({
146568
146972
  code: "TOOL_ERROR",
146569
- message: `key failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(check Accessibility permission)"}`,
146570
- context: { tool: "computer_key", exitCode: proc.exitCode }
146973
+ message: cause ? `key ${cause}` : `key failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(check Accessibility permission)"}`,
146974
+ context: { tool: "computer_key", exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 }
146571
146975
  });
146572
146976
  }
146573
146977
  return { ok: true, key, modifiers: mods };
@@ -146579,6 +146983,12 @@ function buildKeyScript(key, modifiers) {
146579
146983
  if (lower2 in KEY_CODES) {
146580
146984
  return `tell application "System Events" to key code ${KEY_CODES[lower2]}${usingClause}`;
146581
146985
  }
146986
+ if (key.length === 1) {
146987
+ const wsCode = WHITESPACE_KEY_CODES[key];
146988
+ if (wsCode !== void 0) {
146989
+ return `tell application "System Events" to key code ${wsCode}${usingClause}`;
146990
+ }
146991
+ }
146582
146992
  if (key.length === 1) {
146583
146993
  const literal3 = `"${key.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
146584
146994
  return `tell application "System Events" to keystroke ${literal3}${usingClause}`;
@@ -146630,10 +147040,11 @@ var openTool = defineTool({
146630
147040
  timeoutMs: 1e4
146631
147041
  });
146632
147042
  if (proc.exitCode !== 0) {
147043
+ const cause = procFailureCause(proc, 1e4);
146633
147044
  throw new MoxxyError({
146634
147045
  code: "TOOL_ERROR",
146635
- message: `open failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(no error message)"}`,
146636
- context: { tool: "computer_open", exitCode: proc.exitCode }
147046
+ message: cause ? `open ${cause}` : `open failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(no error message)"}`,
147047
+ context: { tool: "computer_open", exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 }
146637
147048
  });
146638
147049
  }
146639
147050
  return { ok: true, app, target };
@@ -146664,7 +147075,8 @@ var screenshotTool = defineTool({
146664
147075
  const fmt2 = format ?? DEFAULT_FORMAT;
146665
147076
  const dim3 = maxDim ?? DEFAULT_MAX_DIM;
146666
147077
  const q3 = quality ?? DEFAULT_JPEG_QUALITY;
146667
- const captureTmp = path3.join(os5.tmpdir(), `moxxy-screencap-${process.pid}-${Date.now()}.png`);
147078
+ const uniq = randomUUID();
147079
+ const captureTmp = path3.join(os5.tmpdir(), `moxxy-screencap-${process.pid}-${Date.now()}-${uniq}.png`);
146668
147080
  const captureArgs = ["-x", "-t", "png"];
146669
147081
  if (region) {
146670
147082
  captureArgs.push("-R", `${region.x},${region.y},${region.width},${region.height}`);
@@ -146675,14 +147087,15 @@ var screenshotTool = defineTool({
146675
147087
  timeoutMs: 15e3
146676
147088
  });
146677
147089
  if (cap.exitCode !== 0) {
147090
+ const cause = procFailureCause(cap, 15e3);
146678
147091
  throw new MoxxyError({
146679
147092
  code: "TOOL_ERROR",
146680
- message: `screencapture failed (exit ${cap.exitCode}): ${cap.stderr.trim() || "(no stderr \u2014 likely Screen Recording permission missing \u2014 grant in System Settings \u2192 Privacy & Security)"}`,
146681
- context: { tool: "computer_screenshot", exitCode: cap.exitCode }
147093
+ message: cause ? `screencapture ${cause}` : `screencapture failed (exit ${cap.exitCode}): ${cap.stderr.trim() || "(no stderr \u2014 likely Screen Recording permission missing \u2014 grant in System Settings \u2192 Privacy & Security)"}`,
147094
+ context: { tool: "computer_screenshot", exitCode: cap.exitCode, timedOut: cap.timedOut ? 1 : 0 }
146682
147095
  });
146683
147096
  }
146684
147097
  const outExt = fmt2 === "jpeg" ? "jpg" : "png";
146685
- const outTmp = path3.join(os5.tmpdir(), `moxxy-screencap-${process.pid}-${Date.now()}-out.${outExt}`);
147098
+ const outTmp = path3.join(os5.tmpdir(), `moxxy-screencap-${process.pid}-${Date.now()}-${uniq}-out.${outExt}`);
146686
147099
  const sipsArgs = [
146687
147100
  "-Z",
146688
147101
  String(dim3),
@@ -146701,10 +147114,11 @@ var screenshotTool = defineTool({
146701
147114
  await promises.rm(captureTmp, { force: true });
146702
147115
  if (sip.exitCode !== 0) {
146703
147116
  await promises.rm(outTmp, { force: true });
147117
+ const cause = procFailureCause(sip, 15e3);
146704
147118
  throw new MoxxyError({
146705
147119
  code: "TOOL_ERROR",
146706
- message: `sips resize/convert failed (exit ${sip.exitCode}): ${sip.stderr.trim() || "(no error message)"}`,
146707
- context: { tool: "computer_screenshot", exitCode: sip.exitCode }
147120
+ message: cause ? `sips resize/convert ${cause}` : `sips resize/convert failed (exit ${sip.exitCode}): ${sip.stderr.trim() || "(no error message)"}`,
147121
+ context: { tool: "computer_screenshot", exitCode: sip.exitCode, timedOut: sip.timedOut ? 1 : 0 }
146708
147122
  });
146709
147123
  }
146710
147124
  try {
@@ -146747,10 +147161,11 @@ var typeTool = defineTool({
146747
147161
  timeoutMs: 3e4
146748
147162
  });
146749
147163
  if (proc.exitCode !== 0) {
147164
+ const cause = procFailureCause(proc, 3e4);
146750
147165
  throw new MoxxyError({
146751
147166
  code: "TOOL_ERROR",
146752
- message: `type failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(check Accessibility permission)"}`,
146753
- context: { tool: "computer_type", exitCode: proc.exitCode }
147167
+ message: cause ? `type ${cause}` : `type failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(check Accessibility permission)"}`,
147168
+ context: { tool: "computer_type", exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 }
146754
147169
  });
146755
147170
  }
146756
147171
  return { ok: true, length: text.length };
@@ -147254,6 +147669,8 @@ var stepSchema = z.object({
147254
147669
  needs: z.array(z.string().min(1)).default([]),
147255
147670
  when: z.string().min(1).optional(),
147256
147671
  onError: z.enum(["fail", "continue", "retry"]).default("fail"),
147672
+ // `retries` only takes effect when `onError: 'retry'`; with 'fail'/'continue'
147673
+ // the step runs exactly one attempt (see runStep in executor/steps.ts).
147257
147674
  retries: z.number().int().min(0).max(3).default(0),
147258
147675
  label: z.string().max(60).optional(),
147259
147676
  format: z.enum(["json", "plain"]).optional(),
@@ -147682,7 +148099,16 @@ async function loadDir2(dir, scope, logger) {
147682
148099
  if (!entry.isFile() || !/\.ya?ml$/i.test(entry.name))
147683
148100
  continue;
147684
148101
  const full = path3.join(dir, entry.name);
147685
- const raw = await promises.readFile(full, "utf8");
148102
+ let raw;
148103
+ try {
148104
+ raw = await promises.readFile(full, "utf8");
148105
+ } catch (err) {
148106
+ logger?.warn?.("workflow: unreadable file, skipping", {
148107
+ path: full,
148108
+ error: err instanceof Error ? err.message : String(err)
148109
+ });
148110
+ continue;
148111
+ }
147686
148112
  const result = parseWorkflowYaml(raw);
147687
148113
  if (!result.ok || !result.workflow) {
147688
148114
  logger?.warn?.("workflow: invalid file, skipping", { path: full, errors: result.errors });
@@ -147719,6 +148145,12 @@ var WorkflowStore = class {
147719
148145
  byName = /* @__PURE__ */ new Map();
147720
148146
  opts;
147721
148147
  loaded = false;
148148
+ // Per-instance lock (invariant: serialize whole-map RMW). A reload
148149
+ // (`load()` clears then refills byName) must never interleave with a save
148150
+ // (read byName → write file → set byName) — e.g. an autonomous onChanged
148151
+ // re-sync racing a user `/workflows enable`, or two concurrent toggles —
148152
+ // or the in-memory registry briefly desyncs from disk.
148153
+ mutex = createMutex();
147722
148154
  constructor(opts) {
147723
148155
  this.opts = opts;
147724
148156
  }
@@ -147730,6 +148162,14 @@ var WorkflowStore = class {
147730
148162
  }
147731
148163
  /** (Re)scan all sources and rebuild the in-memory map. */
147732
148164
  async load() {
148165
+ await this.mutex.run(() => this.loadUnlocked());
148166
+ }
148167
+ /**
148168
+ * Rebuild the map without acquiring the mutex — only call from a context
148169
+ * that already holds it (mutators below), so the clear+refill is atomic
148170
+ * with respect to other mutators.
148171
+ */
148172
+ async loadUnlocked() {
147733
148173
  const discovered = await discoverWorkflows({
147734
148174
  userDir: this.userDir(),
147735
148175
  projectDir: this.projectDir(),
@@ -147742,6 +148182,10 @@ var WorkflowStore = class {
147742
148182
  this.byName.set(wf.workflow.name, wf);
147743
148183
  this.loaded = true;
147744
148184
  }
148185
+ async ensureLoadedUnlocked() {
148186
+ if (!this.loaded)
148187
+ await this.loadUnlocked();
148188
+ }
147745
148189
  async ensureLoaded() {
147746
148190
  if (!this.loaded)
147747
148191
  await this.load();
@@ -147760,17 +148204,19 @@ var WorkflowStore = class {
147760
148204
  }
147761
148205
  /** Write a new workflow file and register it. Rejects duplicate names. */
147762
148206
  async create(workflow, scope) {
147763
- await this.ensureLoaded();
147764
- if (this.byName.has(workflow.name)) {
147765
- throw new Error(`workflow "${workflow.name}" already exists \u2014 use update instead`);
147766
- }
147767
- const dir = scope === "project" ? this.projectDir() : this.userDir();
147768
- await promises.mkdir(dir, { recursive: true });
147769
- const file = await uniqueFilename2(dir, workflow.name);
147770
- await writeFileAtomic(file, serializeWorkflow(workflow));
147771
- const entry = { workflow, path: file, scope };
147772
- this.byName.set(workflow.name, entry);
147773
- return entry;
148207
+ return this.mutex.run(async () => {
148208
+ await this.ensureLoadedUnlocked();
148209
+ if (this.byName.has(workflow.name)) {
148210
+ throw new Error(`workflow "${workflow.name}" already exists \u2014 use update instead`);
148211
+ }
148212
+ const dir = scope === "project" ? this.projectDir() : this.userDir();
148213
+ await promises.mkdir(dir, { recursive: true });
148214
+ const file = await uniqueFilename2(dir, workflow.name);
148215
+ await writeFileAtomic(file, serializeWorkflow(workflow));
148216
+ const entry = { workflow, path: file, scope };
148217
+ this.byName.set(workflow.name, entry);
148218
+ return entry;
148219
+ });
147774
148220
  }
147775
148221
  /**
147776
148222
  * Replace a workflow with a new definition. In-place rewrite for user/project
@@ -147781,7 +148227,10 @@ var WorkflowStore = class {
147781
148227
  * rename doesn't leave an orphaned duplicate file + stale entry behind.
147782
148228
  */
147783
148229
  async save(workflow, previousName) {
147784
- await this.ensureLoaded();
148230
+ return this.mutex.run(() => this.saveUnlocked(workflow, previousName));
148231
+ }
148232
+ async saveUnlocked(workflow, previousName) {
148233
+ await this.ensureLoadedUnlocked();
147785
148234
  const renamed = previousName != null && previousName !== workflow.name ? this.byName.get(previousName) : void 0;
147786
148235
  if (renamed && (renamed.scope === "user" || renamed.scope === "project")) {
147787
148236
  await promises.rm(renamed.path, { force: true });
@@ -147806,24 +148255,28 @@ var WorkflowStore = class {
147806
148255
  }
147807
148256
  /** Toggle a workflow's `enabled` flag, persisting the change. */
147808
148257
  async setEnabled(name, enabled) {
147809
- await this.ensureLoaded();
147810
- const existing = this.byName.get(name);
147811
- if (!existing)
147812
- return null;
147813
- return this.save({ ...existing.workflow, enabled });
148258
+ return this.mutex.run(async () => {
148259
+ await this.ensureLoadedUnlocked();
148260
+ const existing = this.byName.get(name);
148261
+ if (!existing)
148262
+ return null;
148263
+ return this.saveUnlocked({ ...existing.workflow, enabled });
148264
+ });
147814
148265
  }
147815
148266
  /** Delete a user/project workflow file. Read-only scopes cannot be deleted. */
147816
148267
  async delete(name) {
147817
- await this.ensureLoaded();
147818
- const existing = this.byName.get(name);
147819
- if (!existing)
147820
- return { ok: false, reason: "not found" };
147821
- if (existing.scope !== "user" && existing.scope !== "project") {
147822
- return { ok: false, reason: `cannot delete a ${existing.scope} workflow` };
147823
- }
147824
- await promises.rm(existing.path, { force: true });
147825
- this.byName.delete(name);
147826
- return { ok: true };
148268
+ return this.mutex.run(async () => {
148269
+ await this.ensureLoadedUnlocked();
148270
+ const existing = this.byName.get(name);
148271
+ if (!existing)
148272
+ return { ok: false, reason: "not found" };
148273
+ if (existing.scope !== "user" && existing.scope !== "project") {
148274
+ return { ok: false, reason: `cannot delete a ${existing.scope} workflow` };
148275
+ }
148276
+ await promises.rm(existing.path, { force: true });
148277
+ this.byName.delete(name);
148278
+ return { ok: true };
148279
+ });
147827
148280
  }
147828
148281
  };
147829
148282
  async function uniqueFilename2(dir, base2) {
@@ -148127,7 +148580,7 @@ function buildRunResult(ctx, status, ok, extra) {
148127
148580
  };
148128
148581
  }
148129
148582
  async function runStep(step, scope, ctx) {
148130
- const attempts = 1 + Math.max(0, step.retries);
148583
+ const attempts = step.onError === "retry" ? 1 + Math.max(0, step.retries) : 1;
148131
148584
  let lastError = "";
148132
148585
  for (let attempt = 0; attempt < attempts; attempt++) {
148133
148586
  if (ctx.deps.signal.aborted)
@@ -148648,7 +149101,7 @@ ${userMessage.trim()}${FINALIZE_REPLY_SUFFIX}`;
148648
149101
  var DAG_EXECUTOR_NAME = "dag";
148649
149102
  var dagExecutor = defineWorkflowExecutor({
148650
149103
  name: DAG_EXECUTOR_NAME,
148651
- description: "DAG runner: steps with settled dependencies are scheduled in waves of up to `concurrency` ready steps, then executed sequentially within each wave (no overlap yet \u2014 `concurrency` caps the batch size, not wall-clock latency).",
149104
+ description: "DAG runner: steps with settled dependencies are scheduled in waves of up to `concurrency` ready steps, then executed sequentially within each wave (no overlap \u2014 `concurrency` caps the batch size drained per pass, not wall-clock latency).",
148652
149105
  run: runExecutor
148653
149106
  });
148654
149107
 
@@ -148715,7 +149168,7 @@ A workflow is a DAG of steps. Schema:
148715
149168
  - args: templated args object for tool/workflow steps
148716
149169
  - needs: [ <upstream step ids> ] (defines the DAG; omit only for true sources)
148717
149170
  - when (optional, legacy): simple guards only \u2014 '{{ steps.x.output }} is not empty'. Do NOT use when for semantic decisions (use condition/switch).
148718
- - onError (optional): fail | continue | retry ; retries (optional, 0-3)
149171
+ - onError (optional): fail | continue | retry ; retries (optional, 0-3 \u2014 only applies when onError is retry; fail/continue always run exactly one attempt)
148719
149172
 
148720
149173
  Operator data \u2014 two ways: declare a value the operator can supply UP FRONT as an \`inputs\` field (filled in before Run). To PAUSE mid-run and ask a question whose answer depends on earlier steps, set \`awaitInput: true\` on a prompt or skill step: the workflow pauses, surfaces the step's prompt to the operator, and resumes with their reply once they answer. Prefer \`inputs\` for known-up-front values; use \`awaitInput\` only for genuinely mid-run questions.
148721
149174
 
@@ -149501,46 +149954,61 @@ function buildWorkflowRunner(args) {
149501
149954
  }
149502
149955
  async function resumeNow(runId, reply2) {
149503
149956
  const checkpoint = await defaultWorkflowRunStore.load(runId);
149504
- const turnId = session.startTurn().turnId;
149505
- const spawner = createSubagentSpawner({
149506
- parentSession: session,
149507
- parentTurnId: turnId,
149508
- parentSignal: session.signal,
149509
- parentModel: activeModel(session)
149510
- });
149511
- const result = await resumeWorkflowRun(
149512
- runId,
149513
- reply2,
149514
- {
149515
- spawner,
149516
- tools: session.tools,
149517
- lookup: {
149518
- skill: (n2) => session.skills.byName(n2),
149519
- workflow: (n2) => store.lookup(n2)
149520
- },
149521
- signal: session.signal,
149522
- now: () => Date.now(),
149523
- emit: (subtype, payload) => void session.log.append({
149524
- type: "plugin_event",
149525
- sessionId: session.id,
149526
- turnId,
149527
- source: "plugin",
149528
- pluginId: PLUGIN_ID3,
149529
- subtype,
149530
- payload
149531
- }),
149532
- ...logger ? { logger } : {}
149533
- },
149534
- defaultWorkflowRunStore
149535
- );
149536
- if (result.status === "paused") {
149537
- logger?.warn?.("workflows: run paused again awaiting operator input; not delivering to inbox", {
149538
- runId: result.runId
149957
+ const name = checkpoint?.workflow?.name;
149958
+ if (name && inFlight.has(name)) {
149959
+ return {
149960
+ ok: false,
149961
+ status: "failed",
149962
+ steps: [],
149963
+ output: "",
149964
+ error: `workflow "${name}" is already running`
149965
+ };
149966
+ }
149967
+ if (name) inFlight.add(name);
149968
+ try {
149969
+ const turnId = session.startTurn().turnId;
149970
+ const spawner = createSubagentSpawner({
149971
+ parentSession: session,
149972
+ parentTurnId: turnId,
149973
+ parentSignal: session.signal,
149974
+ parentModel: activeModel(session)
149539
149975
  });
149976
+ const result = await resumeWorkflowRun(
149977
+ runId,
149978
+ reply2,
149979
+ {
149980
+ spawner,
149981
+ tools: session.tools,
149982
+ lookup: {
149983
+ skill: (n2) => session.skills.byName(n2),
149984
+ workflow: (n2) => store.lookup(n2)
149985
+ },
149986
+ signal: session.signal,
149987
+ now: () => Date.now(),
149988
+ emit: (subtype, payload) => void session.log.append({
149989
+ type: "plugin_event",
149990
+ sessionId: session.id,
149991
+ turnId,
149992
+ source: "plugin",
149993
+ pluginId: PLUGIN_ID3,
149994
+ subtype,
149995
+ payload
149996
+ }),
149997
+ ...logger ? { logger } : {}
149998
+ },
149999
+ defaultWorkflowRunStore
150000
+ );
150001
+ if (result.status === "paused") {
150002
+ logger?.warn?.("workflows: run paused again awaiting operator input; not delivering to inbox", {
150003
+ runId: result.runId
150004
+ });
150005
+ return result;
150006
+ }
150007
+ if (checkpoint?.workflow) await deliverToInbox(checkpoint.workflow, result, logger);
149540
150008
  return result;
150009
+ } finally {
150010
+ if (name) inFlight.delete(name);
149541
150011
  }
149542
- if (checkpoint?.workflow) await deliverToInbox(checkpoint.workflow, result, logger);
149543
- return result;
149544
150012
  }
149545
150013
  return { runNow, resumeNow };
149546
150014
  }
@@ -150018,7 +150486,7 @@ function buildSchedulerRunner(session) {
150018
150486
  for await (const event of runTurn(session, prompt, model ? { model } : {})) {
150019
150487
  if (event.type === "assistant_message") {
150020
150488
  text = event.content;
150021
- if (event.stopReason === "error") lastError = "turn ended with error stop reason";
150489
+ lastError = event.stopReason === "error" ? "turn ended with error stop reason" : null;
150022
150490
  } else if (event.type === "error") {
150023
150491
  lastError = event.message;
150024
150492
  }
@@ -150043,7 +150511,7 @@ function buildWebhookRunner(session) {
150043
150511
  for await (const event of runTurn(target, prompt, model ? { model } : {})) {
150044
150512
  if (event.type === "assistant_message") {
150045
150513
  text = event.content;
150046
- if (event.stopReason === "error") lastError = "turn ended with error stop reason";
150514
+ lastError = event.stopReason === "error" ? "turn ended with error stop reason" : null;
150047
150515
  } else if (event.type === "error") {
150048
150516
  lastError = event.message;
150049
150517
  }
@@ -150910,7 +151378,7 @@ async function fetchLatest(pkg = DEFAULT_PKG, opts = {}) {
150910
151378
  }
150911
151379
 
150912
151380
  // src/update/check.ts
150913
- var CACHE_TTL_MS = 12 * 60 * 60 * 1e3;
151381
+ var CACHE_TTL_MS2 = 12 * 60 * 60 * 1e3;
150914
151382
  var PKG = "@moxxy/cli";
150915
151383
  function defaultCacheFile() {
150916
151384
  return moxxyPath("update-check.json");
@@ -150965,7 +151433,7 @@ async function checkForCliUpdate(current, opts = {}) {
150965
151433
  const file = opts.cacheFile ?? defaultCacheFile();
150966
151434
  if (!opts.force) {
150967
151435
  const cache4 = readCache(file);
150968
- if (cache4 && now - cache4.checkedAt < CACHE_TTL_MS) {
151436
+ if (cache4 && now - cache4.checkedAt < CACHE_TTL_MS2) {
150969
151437
  return shape(current, cache4.latest);
150970
151438
  }
150971
151439
  }
@@ -151179,12 +151647,14 @@ async function collectKey(providerId, controller) {
151179
151647
  if (!controller.testKey) return value;
151180
151648
  const s2 = ft2();
151181
151649
  s2.start(`Validating ${providerId} key`);
151650
+ let rejected = false;
151182
151651
  try {
151183
151652
  const result = await controller.testKey(providerId, value);
151184
151653
  if (result.ok) {
151185
151654
  s2.stop(`${colors.bold("\u2713")} ${providerId} key looks good`);
151186
151655
  return value;
151187
151656
  }
151657
+ rejected = true;
151188
151658
  s2.stop(`${colors.red("\u2717")} ${providerId} rejected the key: ${result.message}`);
151189
151659
  } catch (err) {
151190
151660
  s2.stop(
@@ -151197,6 +151667,7 @@ async function collectKey(providerId, controller) {
151197
151667
  });
151198
151668
  const retry = guard(retryRaw);
151199
151669
  if (!retry) {
151670
+ if (rejected) bail();
151200
151671
  return value;
151201
151672
  }
151202
151673
  }
@@ -152435,7 +152906,7 @@ ${available}`
152435
152906
  async function runChannelsCommand(argv) {
152436
152907
  const [name, sub, ...rest] = argv.positional;
152437
152908
  if (!name || name === "list") {
152438
- return runList2();
152909
+ return runList2(argv);
152439
152910
  }
152440
152911
  const outcome = await probeSession(
152441
152912
  argvToSetupOptions(argv, {
@@ -152489,12 +152960,13 @@ ${available}`
152489
152960
  if (outcome !== "run-channel") return outcome.code;
152490
152961
  return runChannelByName(name, argv);
152491
152962
  }
152492
- async function runList2() {
152963
+ async function runList2(argv) {
152493
152964
  const { entries, config } = await probeSession(
152494
- argvToSetupOptions(
152495
- { flags: {} },
152496
- { skipKeyPrompt: true, tolerateNoProvider: true, skipProviderActivation: true }
152497
- ),
152965
+ argvToSetupOptions(argv, {
152966
+ skipKeyPrompt: true,
152967
+ tolerateNoProvider: true,
152968
+ skipProviderActivation: true
152969
+ }),
152498
152970
  async ({ session, vault, config: config2 }) => ({
152499
152971
  config: config2,
152500
152972
  entries: await session.channels.listWithAvailability({
@@ -153171,7 +153643,7 @@ function unitPath(spec) {
153171
153643
  }
153172
153644
  function renderUnit(spec, ctx) {
153173
153645
  const execStart = [ctx.node, ctx.cli, ...spec.execArgs].map(quote).join(" ");
153174
- const envLines = Object.entries(spec.env ?? {}).map(([k3, v3]) => `Environment=${k3}=${v3}`).join("\n");
153646
+ const envLines = Object.entries(spec.env ?? {}).map(([k3, v3]) => `Environment=${k3}=${envValue(v3)}`).join("\n");
153175
153647
  return `[Unit]
153176
153648
  Description=${spec.description}
153177
153649
  After=network-online.target
@@ -153195,6 +153667,11 @@ function quote(s2) {
153195
153667
  if (!/[\s"]/.test(s2)) return s2;
153196
153668
  return '"' + s2.replace(/"/g, '\\"') + '"';
153197
153669
  }
153670
+ function envValue(v3) {
153671
+ const s2 = v3.replace(/[\r\n]+/g, " ");
153672
+ if (!/[\s"\\]/.test(s2)) return s2;
153673
+ return '"' + s2.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
153674
+ }
153198
153675
  var systemdService = {
153199
153676
  platform: "linux",
153200
153677
  async getStatus(spec) {
@@ -153423,21 +153900,23 @@ async function runDaemonBackground() {
153423
153900
  return result.ok ? 0 : 1;
153424
153901
  }
153425
153902
  async function runDaemonForeground() {
153426
- const { session, scheduler } = await setupSessionWithConfig({ cwd: process.cwd() });
153903
+ const { session } = await setupSessionWithConfig({ cwd: process.cwd() });
153427
153904
  process.stdout.write(
153428
153905
  `${colors.bold("scheduler daemon")} ${colors.dim("provider=" + (session.providers.getActiveName() ?? "(none)"))}
153429
153906
  ` + colors.dim(" ^C to stop. Schedules fire while this process is alive.\n")
153430
153907
  );
153431
153908
  let stopRequested = false;
153432
- const shutdown = async () => {
153909
+ const shutdown = async (signal) => {
153433
153910
  if (stopRequested) return;
153434
153911
  stopRequested = true;
153912
+ const force = setTimeout(() => process.exit(0), 4e3);
153913
+ force.unref?.();
153435
153914
  process.stdout.write("\nstopping scheduler\u2026\n");
153436
- await scheduler.poller.stop();
153915
+ await session.close(signal).catch(() => void 0);
153437
153916
  process.exit(0);
153438
153917
  };
153439
- process.on("SIGINT", () => void shutdown());
153440
- process.on("SIGTERM", () => void shutdown());
153918
+ process.on("SIGINT", () => void shutdown("SIGINT"));
153919
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
153441
153920
  setInterval(() => {
153442
153921
  }, 6e4);
153443
153922
  return await new Promise(() => {