@@ -14,7 +14,7 @@ Requirements record: `docs/PRD.md` in mosaic-brain (issue #52). Design locked wi
|
||||
/goal <text> set (or replace, with a notify) the active goal
|
||||
/goal --max N <text> set with a non-default run limit (default 25)
|
||||
/goal stop pause the loop, goal retained
|
||||
/goal resume continue a paused goal (kicks a check turn)
|
||||
/goal resume reconcile a paused or waiting goal (kicks one check turn)
|
||||
/goal clear remove the goal entirely
|
||||
```
|
||||
|
||||
@@ -24,7 +24,7 @@ Requirements record: `docs/PRD.md` in mosaic-brain (issue #52). Design locked wi
|
||||
|
||||
1. `before_agent_start` appends the active goal and reporting instructions to the system
|
||||
prompt every turn, so the goal survives context growth and compaction.
|
||||
2. When the agent settles without the goal being satisfied, the extension injects a check
|
||||
2. When the agent settles with ready work and no unresolved wait, the extension injects a check
|
||||
prompt (`sendUserMessage`), forcing the next turn: the check<->proceed loop. Before reporting,
|
||||
follow the continuation loop in `skills-local/ms-proactive-agent/SKILL.md`.
|
||||
3. The agent reports via `goal_report`, the loop's only exit:
|
||||
@@ -87,15 +87,26 @@ The suite includes pure state, persistence, incarnation fencing, exact report-on
|
||||
duplicate evidence, legitimate wait, headless extension-runtime, and enforcement-neutralization
|
||||
red controls.
|
||||
|
||||
## Bounded waits (operator opt-in)
|
||||
## Quiet waits and optional deadlines
|
||||
|
||||
Use `/goal --wait-timeout 60 <goal text>` to suspend automatic checks during a
|
||||
reported wait. The flag accepts 10–86400 seconds and combines with `--max N`.
|
||||
Omitting it preserves the previous wait-loop behavior. Existing running sessions
|
||||
must `/reload` to load this patch; do not reload unrelated seats for a trial.
|
||||
Every accepted wait suspends automatic checks, including untimed waits. No
|
||||
cooldown or model polling is used. A next-check condition describes the dependency;
|
||||
it is not executed as code, and the extension does not register a watch for it.
|
||||
Existing external message/watch delivery or explicit `/goal resume` can start
|
||||
reconciliation. Incoming messages alone do not clear the wait. The model must
|
||||
verify the dependency and report substantive progress or completion. Unrelated
|
||||
input preserves the wait. Explicit resume clears the old wait; another unresolved
|
||||
wait report suspends checks again. Paused and blocked goals still require explicit
|
||||
operator resume, not merely an incoming message.
|
||||
|
||||
Use `/goal --wait-timeout 60 <goal text>` to add one deadline wake to a wait.
|
||||
The flag accepts 10–86400 seconds and combines with `--max N`. Without it,
|
||||
no deadline timer is armed. Existing running sessions require a controlled reload
|
||||
after an approved deployment; do not reload unrelated seats for a trial.
|
||||
|
||||
A valid `goal_report` with `status: "in_progress"`, `progress.kind: "wait"`, an
|
||||
owner, and a watch id or concrete `nextCheck` persists the deadline and yields.
|
||||
owner, and a watch id or concrete `nextCheck` persists the wait and yields.
|
||||
A deadline is persisted only when the operator configured one.
|
||||
There are no model heartbeats during that wait. Repeated wait reports preserve
|
||||
its original deadline. A substantive progress report clears the wait. The
|
||||
extension neither evaluates arbitrary shell conditions nor sends messages to
|
||||
@@ -124,6 +135,11 @@ Late reports cannot settle a paused goal. Reports also refuse if the goal change
|
||||
while an asynchronous policy check is running. State snapshots use atomic file
|
||||
replacement; failed saves stop continuation instead of claiming a saved result.
|
||||
|
||||
The tests exercise unchanged waits, deadline dispatch and observation, unresolved
|
||||
An in-memory pending-check guard prevents duplicate settle/startup events from
|
||||
queuing the same check before its matching start is observed. It does not cancel
|
||||
messages already queued by Pi, span independent extension instances, or supply a
|
||||
process supervisor. Wait state itself persists across same-incarnation reloads.
|
||||
|
||||
The tests exercise timed and untimed waits, deadline dispatch and observation, unresolved
|
||||
pause, compaction, reload, cancellation, print mode, malformed persisted waits,
|
||||
and actual filesystem write failure. This is separate from live seat acceptance.
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
type GoalState,
|
||||
type ProgressDetails,
|
||||
} from "./lib/state.ts";
|
||||
import { decideSettle } from "./lib/settle.ts";
|
||||
import { decideSettle, hasUnresolvedWait } from "./lib/settle.ts";
|
||||
import { parseGoalCommand } from "./lib/parse.ts";
|
||||
import { loadState, saveState, resolveStatePath } from "./lib/store.ts";
|
||||
import { goalPolicyDenialCode, validateAttestedGoalReport } from "./lib/executive-update.ts";
|
||||
@@ -53,6 +53,8 @@ export default function goalExtension(pi: ExtensionAPI) {
|
||||
let waitTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let armedDeadline: string | undefined;
|
||||
let deadlinePending = false;
|
||||
// Process-local queue latch. Only observing this check's start releases it.
|
||||
let pendingCheck: string | undefined;
|
||||
|
||||
function cancelWaitTimer(): void {
|
||||
if (waitTimer !== undefined) clearTimeout(waitTimer);
|
||||
@@ -172,7 +174,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
||||
|
||||
/** Inject one check prompt: immediately when idle, queued behind a running agent otherwise. */
|
||||
function beginCheck(ctx: ExtensionContext): void {
|
||||
if (dead || state.status !== "active") return;
|
||||
if (dead || state.status !== "active" || hasUnresolvedWait(state) || pendingCheck) return;
|
||||
// The loop needs a persistent session. Print mode exits after the prompt
|
||||
// pipeline completes; a forced turn there races teardown (measured). The
|
||||
// goal directive (before_agent_start) still applies in print mode.
|
||||
@@ -180,7 +182,8 @@ export default function goalExtension(pi: ExtensionAPI) {
|
||||
state = recordCheckInjected(state);
|
||||
if (!persist(ctx)) return;
|
||||
renderWidget(ctx);
|
||||
const msg = checkPrompt();
|
||||
const msg = `${checkPrompt()} [goal dispatch ${randomUUID()}]`;
|
||||
pendingCheck = msg;
|
||||
if (ctx.isIdle()) {
|
||||
try {
|
||||
pi.sendUserMessage(msg);
|
||||
@@ -216,6 +219,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
||||
|
||||
function pauseWith(ctx: ExtensionContext, reason: string, notice: string): void {
|
||||
state = pauseGoal(state, reason);
|
||||
pendingCheck = undefined;
|
||||
persist(ctx);
|
||||
renderWidget(ctx);
|
||||
safeNotify(ctx, `goal: ${notice}`, "warning");
|
||||
@@ -247,16 +251,18 @@ export default function goalExtension(pi: ExtensionAPI) {
|
||||
return;
|
||||
}
|
||||
state = clearGoal(state);
|
||||
pendingCheck = undefined;
|
||||
if (!persist(ctx)) return;
|
||||
renderWidget(ctx);
|
||||
safeNotify(ctx, "goal: cleared — normal operation", "info");
|
||||
return;
|
||||
case "resume":
|
||||
if (state.status !== "paused" && state.status !== "blocked") {
|
||||
safeNotify(ctx, "goal: nothing paused to resume", "warning");
|
||||
if (state.status !== "paused" && state.status !== "blocked" && !hasUnresolvedWait(state)) {
|
||||
safeNotify(ctx, "goal: nothing paused or waiting to resume", "warning");
|
||||
return;
|
||||
}
|
||||
state = resumeGoal(state);
|
||||
pendingCheck = undefined;
|
||||
if (!persist(ctx)) return;
|
||||
renderWidget(ctx);
|
||||
safeNotify(ctx, "goal: resumed", "info");
|
||||
@@ -265,6 +271,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
||||
case "set": {
|
||||
const replacing = state.status !== "none";
|
||||
state = setGoal(state, cmd.text, cmd.max, cmd.waitTimeoutSeconds);
|
||||
pendingCheck = undefined;
|
||||
if (!persist(ctx)) return;
|
||||
renderWidget(ctx);
|
||||
safeNotify(
|
||||
@@ -414,7 +421,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
||||
: outcome.classification === "waiting"
|
||||
? (state.waitTimeoutSeconds && ctx.mode !== "print"
|
||||
? `Waiting recorded. Automatic goal checks are suspended until a relevant incoming message or the deadline at ${new Date(state.activeWait!.deadlineAt!).toISOString()}. The extension owns this single deadline wake; do not add timers or report unchanged status. Checkpoint and yield now.`
|
||||
: (ctx.mode === "print" ? "Waiting recorded. Print mode cannot schedule a wake; resume this goal in a persistent session." : `Recorded explicit waiting state (${outcome.reason}). Keep the approved watch or next-check condition active.`))
|
||||
: (ctx.mode === "print" ? "Waiting recorded. Print mode cannot schedule a wake; resume this goal in a persistent session." : "Waiting recorded. Automatic goal checks are suspended. No deadline timer is armed. Keep the existing message/watch delivery path; nextCheck is descriptive, not executable. Reconcile on relevant input or explicit /goal resume. Do not poll or report unchanged status. Checkpoint and yield now."))
|
||||
: `No new progress recorded (${outcome.reason}). No-progress count is ${state.noProgressReports}/${state.maxNoProgressReports}; take the next safe action before reporting again.`;
|
||||
return {
|
||||
content: [{ type: "text", text }],
|
||||
@@ -426,6 +433,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
||||
// ---------- loop wiring ----------
|
||||
|
||||
pi.on("before_agent_start", async (event, ctx) => {
|
||||
if (pendingCheck && event.prompt.includes(pendingCheck)) pendingCheck = undefined;
|
||||
if (state.status !== "active") {
|
||||
if (event.prompt.startsWith("[goal check ")) {
|
||||
return { systemPrompt: `${event.systemPrompt}\nThis queued goal check is obsolete: the goal is paused or cleared. Do not execute it or call goal_report.` };
|
||||
@@ -438,7 +446,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
||||
}
|
||||
const waitingDirective = state.waitTimeoutSeconds && ctx.mode !== "print"
|
||||
? " Bounded waits enabled: report in_progress with progress.kind=wait and a concrete nextCheck when no ready work remains. The extension suspends checks and supplies one deadline wake per goal/resume. Do not create a duplicate timer, sleep loop, or heartbeat. On unrelated incoming messages, keep the wait; on a relevant event verify the dependency. After the deadline wake, an unresolved wait pauses."
|
||||
: "";
|
||||
: " Accepted waits suspend automatic goal checks without a deadline timer. Incoming messages do not clear the wait by themselves: on unrelated input preserve it; on relevant input verify the dependency before reporting progress. Do not poll, add timers, or report unchanged status. The operator can use /goal resume to reconcile once.";
|
||||
const directive =
|
||||
`[goal] ACTIVE OPERATOR GOAL: ${state.text}\n` +
|
||||
`Work toward this goal. Before reporting, run the ms-proactive-agent loop: reconcile records, perform the next authorized ready task, and verify its result. If no task can proceed, report a concrete wait or blocker. This extension owns goal lifecycle; use skills for task tracking, not a second goal loop. ` +
|
||||
@@ -478,6 +486,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
||||
// Teardown guard: after session replacement/reload/quit, stop touching ctx.
|
||||
pi.on("session_shutdown", async () => {
|
||||
dead = true;
|
||||
pendingCheck = undefined;
|
||||
cancelWaitTimer();
|
||||
});
|
||||
|
||||
@@ -492,7 +501,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
||||
safeNotify(ctx, `goal: active — ${truncate(state.text, 60)}`, "info");
|
||||
// Durability (Q7b): a fresh session with an active goal re-engages the
|
||||
// loop immediately — no prior turn exists to emit agent_settled.
|
||||
if (!(state.waitTimeoutSeconds && state.activeWait && !state.activeWait.wakeSent)) beginCheck(ctx);
|
||||
if (!hasUnresolvedWait(state)) beginCheck(ctx);
|
||||
} else if (state.status === "paused" || state.status === "blocked") {
|
||||
safeNotify(ctx, "goal: paused — /goal resume to continue", "info");
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ import { checkLimitReached, type GoalState } from "./state.ts";
|
||||
|
||||
export type SettleDecision = { action: "inject" } | { action: "wait" } | { action: "pause"; reason: string };
|
||||
|
||||
/** An untimed wait never spends model tokens on automatic reconciliation. */
|
||||
export function hasUnresolvedWait(state: GoalState): boolean {
|
||||
return !!state.activeWait && (!state.waitTimeoutSeconds || !state.activeWait.wakeSent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide what the loop does when the agent settles while a goal is active.
|
||||
* stopReason comes from the last assistant message ("stop", "toolUse",
|
||||
@@ -17,7 +22,7 @@ export function decideSettle(state: GoalState, stopReason: string | undefined):
|
||||
if (stopReason === "error") {
|
||||
return { action: "pause", reason: "paused: run error" };
|
||||
}
|
||||
if (state.waitTimeoutSeconds && state.activeWait && !state.activeWait.wakeSent) {
|
||||
if (hasUnresolvedWait(state)) {
|
||||
return { action: "wait" };
|
||||
}
|
||||
if (checkLimitReached(state)) {
|
||||
|
||||
@@ -77,7 +77,7 @@ export interface GoalState {
|
||||
wakeDispatchedAt?: number;
|
||||
wakeObserved?: boolean;
|
||||
};
|
||||
/** Operator opt-in: suspend waits, with one deadline wake per goal/resume. */
|
||||
/** Operator opt-in: add one deadline wake to a suspended wait per goal/resume. */
|
||||
waitTimeoutSeconds?: number;
|
||||
waitWakeUsed?: boolean;
|
||||
setAt: string;
|
||||
@@ -144,7 +144,8 @@ export function blockGoal(state: GoalState, reason: string): GoalState {
|
||||
}
|
||||
|
||||
export function resumeGoal(state: GoalState): GoalState {
|
||||
if (state.status !== "paused" && state.status !== "blocked") return state;
|
||||
if (state.status !== "paused" && state.status !== "blocked" &&
|
||||
!(state.status === "active" && state.activeWait)) return state;
|
||||
return {
|
||||
...state,
|
||||
status: "active",
|
||||
@@ -152,7 +153,7 @@ export function resumeGoal(state: GoalState): GoalState {
|
||||
noProgressReports: 0,
|
||||
workEventSinceReport: false,
|
||||
pausedReason: undefined,
|
||||
activeWait: state.waitTimeoutSeconds ? undefined : state.activeWait,
|
||||
activeWait: undefined,
|
||||
waitWakeUsed: state.waitTimeoutSeconds ? false : state.waitWakeUsed,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,5 +23,5 @@ test("timeout input is bounded and opt-in never leaks to another goal", () => {
|
||||
const opted = setGoal(initialState(), "one", undefined, 60);
|
||||
assert.equal(setGoal(opted, "two").waitTimeoutSeconds, undefined);
|
||||
const legacyWait = recordInProgressReport(setGoal(initialState(), "legacy"), wait).state;
|
||||
assert.equal(decideSettle(legacyWait, "stop").action, "inject");
|
||||
assert.equal(decideSettle(legacyWait, "stop").action, "wait");
|
||||
});
|
||||
|
||||
@@ -51,6 +51,7 @@ async function createHeadlessHarness(root: string, incarnation: string) {
|
||||
const commands = new Map<string, any>();
|
||||
const tools = new Map<string, any>();
|
||||
const sent: SentMessage[] = [];
|
||||
const observedChecks = new Set<string>();
|
||||
const notifications: Array<{ message: string; level: string }> = [];
|
||||
const widgets: unknown[] = [];
|
||||
const statuses: string[] = [];
|
||||
@@ -70,7 +71,18 @@ async function createHeadlessHarness(root: string, incarnation: string) {
|
||||
},
|
||||
registerShortcut(key: string, shortcut: any) { shortcuts.set(key, shortcut); },
|
||||
registerTool(tool: any) {
|
||||
tools.set(tool.name, tool);
|
||||
// Reports execute inside a model turn. Deliver its queued check's start
|
||||
// rather than bypassing before_agent_start as the old fixture did.
|
||||
tools.set(tool.name, { ...tool, execute: async (...args: any[]) => {
|
||||
const queued = sent.at(-1)?.content;
|
||||
if (queued && !observedChecks.has(queued)) {
|
||||
observedChecks.add(queued);
|
||||
for (const handler of handlers.get("before_agent_start") ?? []) {
|
||||
await handler({ prompt: queued, systemPrompt: "base" }, ctx);
|
||||
}
|
||||
}
|
||||
return tool.execute(...args);
|
||||
} });
|
||||
},
|
||||
sendUserMessage(content: string, options?: SentMessage["options"]) {
|
||||
sent.push({ content, options });
|
||||
@@ -113,6 +125,7 @@ async function createHeadlessHarness(root: string, incarnation: string) {
|
||||
idle = value;
|
||||
},
|
||||
async emit(name: string, event: any = { type: name }) {
|
||||
if (name === "before_agent_start") observedChecks.add(event.prompt);
|
||||
for (const handler of handlers.get(name) ?? []) await handler(event, ctx);
|
||||
},
|
||||
state() {
|
||||
@@ -324,6 +337,114 @@ const waitReport = {
|
||||
nextAction: "Read input and finish output", nextCheck: "input file available" },
|
||||
};
|
||||
|
||||
test("queued check latch deduplicates lifecycle events until the matching start", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-check-latch-"));
|
||||
const harness = await createHeadlessHarness(dir, "check-latch");
|
||||
try {
|
||||
await harness.commands.get("goal").handler("ready work", harness.ctx);
|
||||
const first = harness.sent[0].content;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await harness.emit("session_start");
|
||||
await harness.emit("agent_settled");
|
||||
}
|
||||
assert.equal(harness.sent.length, 1);
|
||||
await harness.emit("before_agent_start", { prompt: "unrelated incoming question", systemPrompt: "base" });
|
||||
await harness.emit("agent_settled");
|
||||
assert.equal(harness.sent.length, 1, "unrelated input does not release a pending check");
|
||||
await harness.emit("before_agent_start", { prompt: first, systemPrompt: "base" });
|
||||
await harness.emit("agent_settled");
|
||||
assert.equal(harness.sent.length, 2, "a completed check can continue ready work");
|
||||
await harness.emit("before_agent_start", { prompt: first, systemPrompt: "base" });
|
||||
await harness.emit("agent_settled");
|
||||
assert.equal(harness.sent.length, 2, "duplicate old start cannot release a newer check");
|
||||
await harness.commands.get("goal").handler("replacement", harness.ctx);
|
||||
assert.equal(harness.sent.length, 3, "explicit replacement may schedule its new goal");
|
||||
await harness.commands.get("goal").handler("stop", harness.ctx);
|
||||
await harness.commands.get("goal").handler("resume", harness.ctx);
|
||||
assert.equal(harness.sent.length, 4);
|
||||
} finally {
|
||||
await harness.emit("session_shutdown"); harness.restoreEnv(); rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("untimed wait: settle and duplicate reports never inject a heartbeat", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 100000 });
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-untimed-"));
|
||||
const harness = await createHeadlessHarness(dir, "untimed");
|
||||
try {
|
||||
await harness.commands.get("goal").handler("wait for coordinator", harness.ctx);
|
||||
const report = harness.tools.get("goal_report");
|
||||
await report.execute("wait", waitReport, undefined, undefined, harness.ctx);
|
||||
const before = harness.sent.length;
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await harness.emit("agent_settled");
|
||||
await report.execute(`repeat-${i}`, waitReport, undefined, undefined, harness.ctx);
|
||||
}
|
||||
t.mock.timers.tick(7 * 86400000);
|
||||
assert.equal(harness.sent.length, before, "accepted waits must not generate model turns");
|
||||
assert.equal(harness.state().checks, 0);
|
||||
assert.equal(harness.state().noProgressReports, 0);
|
||||
assert.equal(harness.state().activeWait.deadlineAt, undefined);
|
||||
} finally {
|
||||
await harness.emit("session_shutdown"); harness.restoreEnv(); rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("untimed wait: reload, unrelated input and tool work preserve suspension", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-untimed-reload-"));
|
||||
const harness = await createHeadlessHarness(dir, "untimed-reload");
|
||||
try {
|
||||
await harness.commands.get("goal").handler("wait for coordinator", harness.ctx);
|
||||
const report = harness.tools.get("goal_report");
|
||||
await report.execute("wait", waitReport, undefined, undefined, harness.ctx);
|
||||
const wait = harness.state().activeWait;
|
||||
const before = harness.sent.length;
|
||||
await harness.emit("session_shutdown");
|
||||
await harness.emit("session_start");
|
||||
await harness.emit("session_start");
|
||||
assert.equal(harness.sent.length, before, "startup must preserve even an untimed wait");
|
||||
await harness.emit("before_agent_start", { prompt: "unrelated question", systemPrompt: "base" });
|
||||
await harness.emit("tool_result", { toolName: "read", isError: false });
|
||||
await harness.emit("agent_settled");
|
||||
assert.deepEqual(harness.state().activeWait, wait);
|
||||
assert.equal(harness.sent.length, before);
|
||||
await harness.emit("before_agent_start", { prompt: "dependency result arrived", systemPrompt: "base" });
|
||||
await report.execute("progress", { status: "in_progress", evidence: "dependency verified", progress: { ...waitReport.progress, kind: "measurement", lastMeasurement: "verified input" } }, undefined, undefined, harness.ctx);
|
||||
assert.equal(harness.state().activeWait, undefined);
|
||||
await harness.emit("agent_settled");
|
||||
assert.equal(harness.sent.length, before + 1, "verified progress allows continuation");
|
||||
} finally {
|
||||
await harness.emit("session_shutdown"); harness.restoreEnv(); rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("untimed wait: explicit resume reconciles once, paused waits remain owner-controlled", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-untimed-resume-"));
|
||||
const harness = await createHeadlessHarness(dir, "untimed-resume");
|
||||
try {
|
||||
const goal = harness.commands.get("goal");
|
||||
const report = harness.tools.get("goal_report");
|
||||
await goal.handler("wait for coordinator", harness.ctx);
|
||||
await report.execute("wait", waitReport, undefined, undefined, harness.ctx);
|
||||
await goal.handler("resume", harness.ctx);
|
||||
assert.equal(harness.sent.length, 2, "resume must work for an active waiting goal");
|
||||
assert.equal(harness.state().activeWait, undefined);
|
||||
await report.execute("still-waiting", waitReport, undefined, undefined, harness.ctx);
|
||||
await harness.emit("agent_settled");
|
||||
assert.equal(harness.sent.length, 2);
|
||||
await goal.handler("stop", harness.ctx);
|
||||
await harness.emit("before_agent_start", { prompt: "dependency result", systemPrompt: "base" });
|
||||
await harness.emit("agent_settled");
|
||||
assert.equal(harness.state().status, "paused");
|
||||
assert.equal(harness.sent.length, 2);
|
||||
await goal.handler("resume", harness.ctx);
|
||||
assert.equal(harness.sent.length, 3);
|
||||
assert.equal(harness.state().activeWait, undefined);
|
||||
} finally {
|
||||
await harness.emit("session_shutdown"); harness.restoreEnv(); rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("quiet wait: no heartbeats, one deadline wake, unresolved dependency pauses", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 100000 });
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-quiet-runtime-"));
|
||||
|
||||
Reference in New Issue
Block a user