@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user