@wrongstack/core 0.308.7 → 0.309.0

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.
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import type { EventBus } from '../kernel/events.js';
22
22
  import type { Context } from '../core/context.js';
23
- import type { MailboxAudience, MailboxMessageType } from './mailbox-types.js';
23
+ import type { MailboxMessageType } from './mailbox-types.js';
24
24
  import { type MailboxResolver } from './mailbox-tool.js';
25
25
  export interface MailToolsOptions {
26
26
  /** How to obtain a Mailbox given the execution Context (tests). */
@@ -75,12 +75,16 @@ export declare function makeMailSendTool(opts?: MailToolsOptions): {
75
75
  required: string[];
76
76
  };
77
77
  execute(input: unknown, ctx: Context): Promise<{
78
- messageId?: never;
79
- to?: never;
80
- summary?: never;
81
78
  ok: boolean;
82
79
  error: string;
83
- from?: never;
80
+ } | {
81
+ error?: never;
82
+ ok: boolean;
83
+ messageId: string;
84
+ from: string;
85
+ to: string;
86
+ strippedFields: string[];
87
+ summary: string;
84
88
  } | {
85
89
  error?: never;
86
90
  ok: boolean;
@@ -128,7 +132,7 @@ export declare function makeMailInboxTool(opts?: MailToolsOptions): {
128
132
  from: string;
129
133
  to: string;
130
134
  type: MailboxMessageType;
131
- audience: MailboxAudience;
135
+ audience: import("./mailbox-message-types.js").MailboxAudience;
132
136
  subject: string;
133
137
  body: string;
134
138
  timestamp: string;
@@ -45,6 +45,37 @@ export declare class MailboxValidationError extends Error {
45
45
  readonly field: string;
46
46
  constructor(code: string, field: string, message: string);
47
47
  }
48
+ /** Fields allowed in a send mutation payload from untrusted callers. */
49
+ export declare const SEND_ALLOWED_FIELDS: ReadonlySet<string>;
50
+ /** Result of {@link filterMailboxSendPayload}. */
51
+ export interface FilteredSendPayload {
52
+ /** Copy of the input containing only allow-listed and trust-relevant keys. */
53
+ payload: Record<string, unknown>;
54
+ /** Keys that were removed, in input order. Empty when nothing was stripped. */
55
+ stripped: string[];
56
+ }
57
+ /**
58
+ * Strip fields that do not belong in a send payload before it reaches the
59
+ * boundary codec or the mailbox store.
60
+ *
61
+ * Senders (hosts, adapters, models) attach fields the mailbox never asked
62
+ * for — debug knobs, client metadata, accidental whole-context dumps. Two
63
+ * failure modes follow without a filter: the strict codec rejects the whole
64
+ * send because of one irrelevant key, or a lenient surface persists the
65
+ * clutter into every recipient's inbox. This function prevents both:
66
+ * irrelevant keys are removed from the payload and reported in `stripped`.
67
+ *
68
+ * It is pure (never mutates the input) and keyed off the same
69
+ * {@link SEND_ALLOWED_FIELDS} set that governs `parseMailboxSendInput`, so
70
+ * the filter and the validator cannot drift apart.
71
+ *
72
+ * Trust-relevant fields (`from`, `sessionAffinity`, see
73
+ * {@link SEND_FORBIDDEN_FIELDS}) are deliberately PASSED THROUGH, never
74
+ * stripped: the codec must reject them loudly as unknown fields. Dropping
75
+ * them here would convert a forgery attempt into a successful,
76
+ * differently-scoped send.
77
+ */
78
+ export declare function filterMailboxSendPayload(input: Record<string, unknown>): FilteredSendPayload;
48
79
  /**
49
80
  * Result of successful send-input parsing.
50
81
  * `type` is the resolved type after default selection and validation.
@@ -150,6 +150,20 @@ export declare class DefaultMultiAgentCoordinator extends EventEmitter implement
150
150
  * calls this itself.
151
151
  */
152
152
  completeTask(result: TaskResult): void;
153
+ /**
154
+ * Ask every RUNNING subagent that opted into `gracefulFinish` to finish its
155
+ * task in its own turn (see coordination/subagent-finish.ts). This is the
156
+ * leader-side entry point for "the leader agent has finished": it delivers
157
+ * an in-band notification between tool batches — never an interrupt, never
158
+ * an abort. Each notified subagent keeps its existing time budget and
159
+ * accelerates; the watchdog still bounds the maximum lifetime.
160
+ *
161
+ * Subagents without the policy opted in are deliberately untouched — their
162
+ * lifecycle remains the legacy watchdog contract.
163
+ *
164
+ * Returns the number of subagents actually notified.
165
+ */
166
+ requestFinish(reason: string): number;
153
167
  private tryDispatchNext;
154
168
  private canDispatch;
155
169
  private takeNextDispatchableTask;
@@ -1,5 +1,6 @@
1
1
  import type { SubagentRunContext, SubagentRunner, TaskSpec } from '../types/multi-agent.js';
2
2
  import { type SubagentBudget } from './subagent-budget.js';
3
+ import type { GracefulFinish } from './subagent-finish.js';
3
4
  export interface ExecuteSubagentWithTimeoutOptions {
4
5
  runner: SubagentRunner;
5
6
  task: TaskSpec;
@@ -8,6 +9,15 @@ export interface ExecuteSubagentWithTimeoutOptions {
8
9
  preemptFraction?: number | undefined;
9
10
  abortSubagent: (subagentId: string) => void;
10
11
  currentSessionId: () => string | undefined;
12
+ /**
13
+ * Model-driven completion policy resolved from the subagent config. When
14
+ * set, crossing the wall-clock deadline does NOT abort the runner: the
15
+ * budget emits `subagent.finish_requested` in-band (folded into the
16
+ * conversation between tool batches) and extends its own ceiling by the
17
+ * grace window. The terminal stop applies only if that window also
18
+ * elapses — the subagent's bounded maximum lifetime.
19
+ */
20
+ gracefulFinish?: GracefulFinish | undefined;
11
21
  }
12
- export declare function executeSubagentWithTimeout({ runner, task, ctx, budget, preemptFraction, abortSubagent, currentSessionId, }: ExecuteSubagentWithTimeoutOptions): Promise<import("../types/multi-agent.js").SubagentRunOutcome>;
22
+ export declare function executeSubagentWithTimeout({ runner, task, ctx, budget, preemptFraction, abortSubagent, currentSessionId, gracefulFinish, }: ExecuteSubagentWithTimeoutOptions): Promise<import("../types/multi-agent.js").SubagentRunOutcome>;
13
23
  //# sourceMappingURL=multi-agent-timeout.d.ts.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Deterministic mutation-testing engine ("Kaos Maymunu" / Chaos Monkey).
3
+ *
4
+ * Plans and applies classical boundary-condition mutations to TypeScript
5
+ * source — `>` ↔ `>=`, `+` ↔ `-`, boolean negation, `return x` → `return
6
+ * null`. The engine is deliberately regex/token based, NOT AST based: it has
7
+ * zero dependencies, runs synchronously, and every mutation site is
8
+ * re-derivable from (file, mutation id) alone. Callers (the `mutation_test`
9
+ * director tool) compute the plan, hand it to a chaos-monkey subagent that
10
+ * applies/runs/restores in an isolated worktree, and then compare per-mutant
11
+ * test outcomes against this plan.
12
+ *
13
+ * Safety properties the engine guarantees:
14
+ * - `applyMutation` is a pure string transform: given the same source and
15
+ * the same mutation id it always produces the same output.
16
+ * - Mutations are single-site: exactly one token occurrence changes.
17
+ * - Ids are stable across runs (position-anchored, not hash-of-content), so
18
+ * a worktree-chaos agent and the director tool agree on what each id
19
+ * means without exchanging anything but the id list.
20
+ *
21
+ * @module coordination/mutation-engine
22
+ */
23
+ /** Mutation families this engine can plan. */
24
+ export type MutationKind = 'relax-boundary' | 'tighten-boundary' | 'arith-plus-to-minus' | 'arith-minus-to-plus' | 'negate-boolean' | 'return-null';
25
+ export interface MutationPlanItem {
26
+ /** Stable id: `<kind>#<line1based>#<col1based>`. */
27
+ id: string;
28
+ kind: MutationKind;
29
+ /** Project-relative file path, exactly as passed to planMutations. */
30
+ file: string;
31
+ /** 1-based line. */
32
+ line: number;
33
+ /** 1-based column of the mutated token start. */
34
+ column: number;
35
+ /** Original source text at the site. */
36
+ original: string;
37
+ /** Replacement text. */
38
+ replacement: string;
39
+ }
40
+ export interface PlanMutationsOptions {
41
+ /** Hard cap on planned mutants per file. Default 25. */
42
+ maxPerFile?: number | undefined;
43
+ }
44
+ /**
45
+ * Plan mutations for one file's source text.
46
+ *
47
+ * The scan is line-by-line with the file's own line splits preserved so ids
48
+ * stay (line, column) anchored. Mutations inside comments and string
49
+ * literals are filtered out by `isMasked` below.
50
+ */
51
+ export declare function planMutations(file: string, source: string, opts?: PlanMutationsOptions): MutationPlanItem[];
52
+ /**
53
+ * Apply one planned mutation to source. Pure: same input → same output.
54
+ * Returns the original source when the site no longer matches (the file has
55
+ * drifted since planning) so callers can treat that as a skipped mutant.
56
+ */
57
+ export declare function applyMutation(source: string, mutation: Pick<MutationPlanItem, 'kind' | 'line' | 'column' | 'original' | 'replacement'>): string;
58
+ /**
59
+ * Parse a structured mutation report emitted by the chaos-monkey subagent
60
+ * (either via `submit_result` or its final text). Tolerant of surrounding
61
+ * prose: the first JSON object containing a `mutants` array wins.
62
+ */
63
+ export declare function parseMutationReport(text: string): {
64
+ mutants: Array<{
65
+ id: string;
66
+ file: string;
67
+ line: number;
68
+ kind: string;
69
+ status: 'killed' | 'survived' | 'skipped';
70
+ evidence?: string | undefined;
71
+ }>;
72
+ summary?: string | undefined;
73
+ } | undefined;
74
+ //# sourceMappingURL=mutation-engine.d.ts.map
@@ -63,6 +63,19 @@ export type BudgetNegotiationMode = 'auto' | 'sync';
63
63
  export type BudgetSessionIdSource = string | (() => string | undefined);
64
64
  interface SubagentBudgetOptions {
65
65
  sessionId?: BudgetSessionIdSource | undefined;
66
+ /** Owning subagent id — used to address the graceful-finish event. */
67
+ subagentId?: string | undefined;
68
+ /**
69
+ * Wall-clock enforcement is owned EXCLUSIVELY by the coordinator watchdog
70
+ * (`executeSubagentWithTimeout`). Set for `gracefulFinish` runs: their
71
+ * notify-then-bound lifecycle must not be raced by `checkTimeout()` calls
72
+ * from `tool.progress` heartbeats, which can fire in the window between a
73
+ * deadline crossing and the watchdog's own tick — starting legacy
74
+ * negotiation that either aborts before `subagent.finish_requested` is
75
+ * delivered or grants an extension past the grace deadline (violating the
76
+ * bounded maximum lifetime). Idle-timeout checks still run.
77
+ */
78
+ wallClockWatchdogOwned?: boolean | undefined;
66
79
  }
67
80
  export interface BudgetUsage {
68
81
  iterations: number;
@@ -146,6 +159,43 @@ export declare class SubagentBudget {
146
159
  * This is the single write path for limit mutations so that future
147
160
  * validation or side-effects live in one place (M1). */
148
161
  patchLimits(ext: Partial<BudgetLimits>): void;
162
+ /**
163
+ * Graceful-finish state (see coordination/subagent-finish.ts).
164
+ * `_finishNotified` guards the single in-band emission; `_grace` records a
165
+ * granted working-time extension past the original wall-clock deadline.
166
+ * They are separate because the two callers want different semantics:
167
+ * the watchdog grants grace at the deadline crossing (notify + extend),
168
+ * while an explicit leader-finished request only notifies — a subagent
169
+ * well inside its budget keeps its full legitimate working time and simply
170
+ * accelerates.
171
+ */
172
+ private _finishNotified;
173
+ private _grace;
174
+ /** True once the in-band finish notification has been emitted. */
175
+ get finishNotified(): boolean;
176
+ /** True once a grace window has been granted past the original deadline. */
177
+ get graceGranted(): boolean;
178
+ /**
179
+ * Notify the subagent in-band to finish its task in its own turn:
180
+ * `subagent.finish_requested` is emitted on the wired EventBus and the
181
+ * agent loop folds the notice into the conversation between tool batches.
182
+ * Nothing aborts — this is a notification, never an interrupt.
183
+ *
184
+ * `opts.graceMs` additionally extends the wall-clock ceiling by that window
185
+ * (used by the watchdog at a deadline crossing, so the model gets working
186
+ * time instead of a kill). Omit it to notify without touching the budget —
187
+ * the subagent keeps its existing time budget and just accelerates.
188
+ *
189
+ * Returns `true` when this call did something (emitted the notification
190
+ * and/or granted grace); `false` when there was nothing to do (already
191
+ * notified, grace already granted, no EventBus wired, budget not started).
192
+ */
193
+ notifyFinish(reason: string, opts?: {
194
+ graceMs?: number | undefined;
195
+ }, now?: () => number): boolean;
196
+ /** Epoch ms by which the subagent should have produced its final output,
197
+ * once a grace window was granted. Undefined before that. */
198
+ get finishDeadlineMs(): number | undefined;
149
199
  private iterations;
150
200
  private toolCalls;
151
201
  private tokenInput;
@@ -161,6 +211,10 @@ export declare class SubagentBudget {
161
211
  private lastActivityTime;
162
212
  private _onThreshold;
163
213
  private readonly _sessionId;
214
+ /** Owning subagent id — used to address the graceful-finish event. */
215
+ private readonly _subagentId;
216
+ /** True when only the coordinator watchdog may enforce wall-clock limits. */
217
+ private readonly _wallClockWatchdogOwned;
164
218
  /**
165
219
  * Hard cap on how long `_negotiateExtension` waits for the coordinator to
166
220
  * respond before defaulting to 'stop'. Without this fallback an absent
@@ -0,0 +1,78 @@
1
+ /**
2
+ * subagent-finish — model-driven completion for background subagents.
3
+ *
4
+ * Problem this module solves: a subagent that outlives its leader (the
5
+ * post-session Chimera reviewer is the canonical case) previously had only
6
+ * two exits — finish on its own, or be killed by the wall-clock watchdog
7
+ * (`executeSubagentWithTimeout` aborts the runner at the deadline). The kill
8
+ * is an external interrupt: it discards whatever the model was mid-way
9
+ * through producing and violates the "agent completes its own turn" contract.
10
+ *
11
+ * The graceful-finish path replaces the kill with a notification:
12
+ *
13
+ * 1. The subagent's config opts in via `gracefulFinish`.
14
+ * 2. When the finish condition fires — the wall-clock deadline is crossed,
15
+ * or the leader explicitly calls `Director.requestFinish()` — the budget
16
+ * records a single finish deadline and emits `subagent.finish_requested`
17
+ * on the subagent's EventBus, carrying the ready-to-read notice text.
18
+ * 3. The agent loop folds that notice into the conversation as a `/btw`
19
+ * note at the TOP of its next iteration — between tool batches,
20
+ * in-band, no abort, no restart.
21
+ * 4. The model reads the notice and completes its task in its own turn.
22
+ * 5. If the grace window also elapses, the watchdog applies the existing
23
+ * terminal stop. That is the documented maximum lifetime — the bound
24
+ * that keeps a subagent from living forever, reached only after the
25
+ * model was given legitimate working time to finish.
26
+ *
27
+ * @module subagent-finish
28
+ */
29
+ /** Structural form of `SubagentConfig.gracefulFinish` (avoids a layer cycle). */
30
+ export type GracefulFinishConfig = boolean | {
31
+ graceMs?: number | undefined;
32
+ } | undefined;
33
+ /**
34
+ * EventBus name for the in-band finish request. Emitted on the subagent's own
35
+ * EventBus (the bus the runner wired into `budget._events`), so delivery is
36
+ * process-local and lands at the loop's next iteration boundary.
37
+ */
38
+ export declare const SUBAGENT_FINISH_REQUESTED_EVENT = "subagent.finish_requested";
39
+ /**
40
+ * Default grace window granted after the finish request fires. Long enough
41
+ * for a model to run one or two more tool calls and write a complete final
42
+ * report; short enough that a session shutdown cannot stall indefinitely.
43
+ */
44
+ export declare const DEFAULT_SUBAGENT_FINISH_GRACE_MS = 120000;
45
+ /** Resolved graceful-finish policy for one subagent. */
46
+ export interface GracefulFinish {
47
+ /** Milliseconds of legitimate working time granted after notification. */
48
+ graceMs: number;
49
+ }
50
+ /**
51
+ * Resolve the graceful-finish policy from a `SubagentConfig`.
52
+ *
53
+ * `undefined` (the default for every existing spawn) keeps the legacy
54
+ * behavior byte-for-byte: the watchdog preempts/negotiates/aborts exactly as
55
+ * it did before. Only spawns that explicitly opt in get the notify-then-bound
56
+ * lifecycle.
57
+ */
58
+ export declare function resolveGracefulFinish(config: {
59
+ gracefulFinish?: GracefulFinishConfig | undefined;
60
+ }): GracefulFinish | undefined;
61
+ /**
62
+ * The in-band notice the subagent reads between tool calls. Delivered as a
63
+ * `/btw` note, so it arrives at the next iteration boundary — never mid-tool,
64
+ * never as an abort. The wording is deliberately imperative and
65
+ * self-contained: the model must be able to act on it without any other
66
+ * context about why the leader finished. Carried verbatim on the event
67
+ * payload so the core agent loop needs no coordination-layer import to fold
68
+ * it into the conversation.
69
+ */
70
+ export declare function buildSubagentFinishNotice(input: {
71
+ /** Why the finish was requested (e.g. "leader session ended"). */
72
+ reason: string;
73
+ /** Epoch milliseconds by which the final output should be complete. */
74
+ deadlineMs: number;
75
+ /** Granted working-time window in milliseconds. */
76
+ graceMs: number;
77
+ }): string;
78
+ //# sourceMappingURL=subagent-finish.d.ts.map
@@ -3649,9 +3649,17 @@ var TASK_SNIPPET_CHARS = 60;
3649
3649
  function fleetPulseSignature(statuses) {
3650
3650
  return statuses.map((s) => `${s.agentId}|${s.status}|${s.currentTask ?? ""}`).sort().join("\n");
3651
3651
  }
3652
- function peerLine(s) {
3652
+ function visibleLineKey(s) {
3653
+ const role = s.role && s.role !== s.name ? s.role : "";
3654
+ const task = s.currentTask && s.currentTask.length > TASK_SNIPPET_CHARS ? `${s.currentTask.slice(0, TASK_SNIPPET_CHARS)}\u2026` : s.currentTask ?? "";
3655
+ const tool = s.currentTool || "";
3656
+ const toolCalls = s.toolCalls > 0 ? String(s.toolCalls) : "";
3657
+ return [s.name, role, s.status, task, tool, toolCalls].join("\0");
3658
+ }
3659
+ function peerLine(s, count = 1) {
3653
3660
  const role = s.role && s.role !== s.name ? ` (${s.role})` : "";
3654
- const parts = [`\u2022 ${s.name}${role} \u2014 ${s.status}`];
3661
+ const grouped = count > 1 ? ` \xD7${count}` : "";
3662
+ const parts = [`\u2022 ${s.name}${role}${grouped} \u2014 ${s.status}`];
3655
3663
  if (s.currentTask) {
3656
3664
  const task = s.currentTask.length > TASK_SNIPPET_CHARS ? `${s.currentTask.slice(0, TASK_SNIPPET_CHARS)}\u2026` : s.currentTask;
3657
3665
  parts.push(`"${task}"`);
@@ -3667,13 +3675,20 @@ function buildFleetPulseBlock(statuses, opts) {
3667
3675
  if (peers.length === 0) return null;
3668
3676
  const order = { running: 0, streaming: 0, waiting_user: 1, idle: 2, error: 3, offline: 4 };
3669
3677
  const sorted = [...peers].sort(
3670
- (x, y) => (order[x.status] ?? 5) - (order[y.status] ?? 5) || x.name.localeCompare(y.name)
3678
+ (x, y) => (order[x.status] ?? 5) - (order[y.status] ?? 5) || visibleLineKey(x).localeCompare(visibleLineKey(y))
3671
3679
  );
3672
3680
  const shown = sorted.slice(0, maxAgents);
3673
3681
  const hidden = sorted.length - shown.length;
3674
3682
  const parts = [];
3675
3683
  parts.push(`[FLEET PULSE] ${peers.length} peer${peers.length === 1 ? "" : "s"} online:`);
3676
- for (const s of shown) parts.push(peerLine(s));
3684
+ for (let i = 0; i < shown.length; ) {
3685
+ let run = 1;
3686
+ while (i + run < shown.length && visibleLineKey(shown[i]) === visibleLineKey(shown[i + run])) {
3687
+ run++;
3688
+ }
3689
+ parts.push(peerLine(shown[i], run));
3690
+ i += run;
3691
+ }
3677
3692
  if (hidden > 0) parts.push(`\u2026 +${hidden} more`);
3678
3693
  parts.push(
3679
3694
  "[END FLEET PULSE] (FYI \u2014 coordinate via mail_send; avoid duplicating peers' work)"