opencode-wait-a-minute 1.1.1 → 1.2.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.
- package/README.md +1 -1
- package/index.js +1 -3
- package/package.json +1 -1
- package/src/agents/invocation-adapter.js +27 -1
- package/src/context/context.js +3 -13
- package/src/context/selection.js +2 -2
- package/src/execution/execution-gate.js +28 -1
- package/src/integration/message-handler.js +18 -3
- package/src/policy/governance-enforcement.js +20 -0
- package/src/quality/task-quality-contract.js +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ WAM adds deterministic control, task-state management, context reconstruction, s
|
|
|
21
21
|
|
|
22
22
|
WAM ships a CQE adapter (`src/context/cqe-adapter.js`) that integrates [context-query-core](https://github.com/khnker/context-query-core), a fast, in-process code-evidence retriever using SQLite + trigram indexing for lexical and symbolic snippet matching.
|
|
23
23
|
|
|
24
|
-
Modes: `disabled`, `shadow`, `enabled`. The adapter defaults to `enabled` when invoked directly; override with `WAM_CQE_MODE`, or disable with `WAM_CQE=0`.
|
|
24
|
+
Modes: `disabled`, `shadow`, `enabled`. The adapter defaults to `enabled` when invoked directly; override with `WAM_CQE_MODE`, or disable with `WAM_CQE=0`. Every runtime context retrieval consults CQE via `selectContextWithCqe` and augments the package with bounded `file_context`; opt out with `WAM_CQE_MODE=disabled`, `WAM_CQE=0`, or `cfg.disableCqe`.
|
|
25
25
|
|
|
26
26
|
Benchmark (fixture corpus, k=5, 10 queries): CQE retrieved **142 tokens vs. 302 for the baseline** ripgrep search — **52.98% fewer tokens** — with higher precision (0.617 vs 0.613) and MRR (0.700 vs 0.683), and comparable recall (0.70 vs 0.75). Reproduce with:
|
|
27
27
|
|
package/index.js
CHANGED
|
@@ -29,7 +29,7 @@ import { activeTaskPath, sessionTaskPath, tasksDir as wamTasksDir, taskDir as wa
|
|
|
29
29
|
|
|
30
30
|
import { initMemory, updateProjectMemo, summarizeOperationalContext, updateContext, getOperationalContext, updateTaskMemory, addRecentChange, recordDecision, getDecision, listDecisions, updateLiveContext, compactDecisions } from "./src/persistence/memory.js";
|
|
31
31
|
import { purgeTasks } from "./src/persistence/retention-manager.js";
|
|
32
|
-
import { getSessionId, listCapsules, getCapsule, promoteCapsule, selectContext, selectContextWithCqe,
|
|
32
|
+
import { getSessionId, listCapsules, getCapsule, promoteCapsule, selectContext, selectContextWithCqe, retrieveContext, retrieveContextWithCqe, closeSession, resolveWamRoot, migrateLegacyCapsules, setActiveSession, applyBootstrapApproval } from "./src/context/context.js";
|
|
33
33
|
import { assembleContext } from "./src/context/assembly.js";
|
|
34
34
|
import { injectCanonicalProjectContext } from "./src/context/canonical-project-context.js";
|
|
35
35
|
import { evaluateRequirement as evaluateRequirementChecks, verifyRequirement } from "./src/verification/verification.js";
|
|
@@ -1608,7 +1608,6 @@ async function postToolExecution(input, output) {
|
|
|
1608
1608
|
persistLiveContext,
|
|
1609
1609
|
assembleContext,
|
|
1610
1610
|
selectContextWithCqe,
|
|
1611
|
-
detectFileSearchIntent,
|
|
1612
1611
|
createSnapshot,
|
|
1613
1612
|
checkContinuation,
|
|
1614
1613
|
rebuildScope,
|
|
@@ -1627,7 +1626,6 @@ async function postToolExecution(input, output) {
|
|
|
1627
1626
|
promoteCapsule,
|
|
1628
1627
|
selectContext,
|
|
1629
1628
|
selectContextWithCqe,
|
|
1630
|
-
detectFileSearchIntent,
|
|
1631
1629
|
retrieveContext,
|
|
1632
1630
|
closeSession,
|
|
1633
1631
|
resolveWamRoot,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-wait-a-minute",
|
|
3
3
|
"description": "WAM keeps more state than it sends: deterministic task-state, context reconstruction, and evidence-backed completion for OpenCode agents.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.6"
|
|
@@ -336,6 +336,32 @@ function sessionErrorMessage(data) {
|
|
|
336
336
|
return `agent session error: ${detail}${status ? ` (${status})` : ""}`;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
const NON_RETRYABLE_PROMPT_STATUS_CODES = new Set([400, 401, 403, 404, 422]);
|
|
340
|
+
|
|
341
|
+
function promptErrorStatus(error) {
|
|
342
|
+
const raw = error?.data?.statusCode ?? error?.status ?? error?.statusCode;
|
|
343
|
+
const status = Number(raw);
|
|
344
|
+
return Number.isFinite(status) ? status : null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function promptErrorFromResponse(response, sessionID) {
|
|
348
|
+
const cause = response?.info?.error ?? response?.error;
|
|
349
|
+
const status = promptErrorStatus(cause);
|
|
350
|
+
const nonRetryable =
|
|
351
|
+
status !== null && NON_RETRYABLE_PROMPT_STATUS_CODES.has(status);
|
|
352
|
+
let message = sessionErrorMessage(response) ?? "agent session error";
|
|
353
|
+
if (nonRetryable && !message.includes(`(${status})`)) {
|
|
354
|
+
message = `${message} (${status})`;
|
|
355
|
+
}
|
|
356
|
+
return new AgentInvocationError(message, {
|
|
357
|
+
code: nonRetryable
|
|
358
|
+
? AGENT_INVOCATION_ERROR_CODES.INVALID_RESULT
|
|
359
|
+
: AGENT_INVOCATION_ERROR_CODES.PROMPT_FAILED,
|
|
360
|
+
cause,
|
|
361
|
+
sessionID,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
339
365
|
function responseText(parts) {
|
|
340
366
|
if (!Array.isArray(parts)) return "";
|
|
341
367
|
return parts
|
|
@@ -431,7 +457,7 @@ export function createOpenCodeV1AgentAdapter({
|
|
|
431
457
|
{ code: AGENT_INVOCATION_ERROR_CODES.PROMPT_TIMEOUT, sessionID: session.id },
|
|
432
458
|
),
|
|
433
459
|
);
|
|
434
|
-
if (response?.error) throw response.
|
|
460
|
+
if (response?.error) throw promptErrorFromResponse(response, session.id);
|
|
435
461
|
} catch (cause) {
|
|
436
462
|
if (cause instanceof AgentInvocationError) {
|
|
437
463
|
if (!cause.sessionID) cause.sessionID = session.id;
|
package/src/context/context.js
CHANGED
|
@@ -27,15 +27,6 @@ export function loadDesignTokens(root = process.cwd()) {
|
|
|
27
27
|
return ctx.tokens || {};
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export function detectFileSearchIntent(task) {
|
|
31
|
-
if (typeof task !== "string" || !task) return false;
|
|
32
|
-
// Normalize common Spanish diacritics so patterns like "dónde"/"donde" match uniformly.
|
|
33
|
-
const q = task.toLowerCase().normalize("NFD").replace(/\p{Diacritic}/gu, "");
|
|
34
|
-
// English + Spanish intent keywords (intent detection only — does not capture the query itself).
|
|
35
|
-
// `\b` boundaries keep us from matching substrings inside larger words (e.g. "archivo" inside "desarchivo").
|
|
36
|
-
return /\b(find|search|grep|locate|where\s+is|busca|buscar|encuentra|donde|revisa|revise|ver|archivo|archivos|codigo|file|files)\b/.test(q);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
30
|
const LEVELS = ["L1", "L2", "L3", "L4"];
|
|
40
31
|
const LIFECYCLE = ["candidate", "active", "superseded", "stale", "invalidated"];
|
|
41
32
|
const PROVENANCE = ["user_decided", "observed", "inferred"];
|
|
@@ -614,13 +605,12 @@ export function selectContext(task, { budget = 8000, root, sessionId, log = true
|
|
|
614
605
|
|
|
615
606
|
/**
|
|
616
607
|
* Variante async de selectContext que enriquece el paquete con file_context de
|
|
617
|
-
* CQE
|
|
618
|
-
*
|
|
619
|
-
* no pueden esperarlo (p.ej. assembleContext).
|
|
608
|
+
* CQE. Se mantiene separada del selectContext sincrónico para no forzar `await`
|
|
609
|
+
* en callers que no pueden esperarlo (p.ej. assembleContext).
|
|
620
610
|
*/
|
|
621
611
|
export async function selectContextWithCqe(task, opts = {}) {
|
|
622
612
|
const pkg = selectContext(task, opts);
|
|
623
|
-
if (!opts.disableCqe
|
|
613
|
+
if (!opts.disableCqe) {
|
|
624
614
|
const t0 = Date.now();
|
|
625
615
|
try {
|
|
626
616
|
const cqeRes = await retrieveWithCqe(task, { repoRoot: opts.root || process.cwd() });
|
package/src/context/selection.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { selectContext, selectContextWithCqe, retrieveContext
|
|
1
|
+
import { selectContext, selectContextWithCqe, retrieveContext } from "./context.js";
|
|
2
2
|
import { detectCouplingIntent, traceCouplingForTask } from "./coupling.js";
|
|
3
|
-
export { selectContext, selectContextWithCqe, retrieveContext,
|
|
3
|
+
export { selectContext, selectContextWithCqe, retrieveContext, detectCouplingIntent, traceCouplingForTask };
|
|
@@ -32,11 +32,38 @@ export function shouldClearExecutionGate(task, taskInFlight = false) {
|
|
|
32
32
|
// guarded by this one, so it must be treated as stale instead of blocking
|
|
33
33
|
// every mutation forever after a restart. Gates without ownerPid predate this
|
|
34
34
|
// marker and are also stale.
|
|
35
|
+
const DEFAULT_EXECUTION_GATE_TTL_MS = 30 * 60 * 1000;
|
|
36
|
+
|
|
37
|
+
function executionGateTtlMs() {
|
|
38
|
+
const raw = Number(process.env.WAM_EXECUTION_GATE_TTL_MS);
|
|
39
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_EXECUTION_GATE_TTL_MS;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseGateTimestamp(value) {
|
|
43
|
+
if (typeof value === "number") {
|
|
44
|
+
return Number.isFinite(value) ? value : null;
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
47
|
+
const numeric = Number(value);
|
|
48
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
49
|
+
const parsed = Date.parse(value);
|
|
50
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
35
55
|
export function isExecutionGateStale(gate, currentPid = process.pid) {
|
|
36
56
|
if (gate?.active !== true) return false;
|
|
37
57
|
const ownerPid = gate?.ownerPid;
|
|
38
58
|
if (!Number.isInteger(ownerPid)) return true;
|
|
39
|
-
|
|
59
|
+
if (ownerPid !== currentPid) return true;
|
|
60
|
+
// A same-pid gate can still be orphaned (its pass aborted before the
|
|
61
|
+
// `finally` ran), so bound its lifetime instead of trusting the pid alone.
|
|
62
|
+
// Missing or malformed timestamps fail open: a stuck gate must never brick
|
|
63
|
+
// the session.
|
|
64
|
+
const startedAt = parseGateTimestamp(gate?.startedAt);
|
|
65
|
+
if (startedAt === null) return true;
|
|
66
|
+
return Date.now() - startedAt > executionGateTtlMs();
|
|
40
67
|
}
|
|
41
68
|
|
|
42
69
|
// A gate is orphaned when its owning loop reached a terminal state but no
|
|
@@ -16,6 +16,7 @@ import fs from "node:fs";
|
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
|
|
18
18
|
import { hasWamMarkerFor, injectWamParts, isWamSynthetic } from "./part-provenance.js";
|
|
19
|
+
import { logger } from "./logger.js";
|
|
19
20
|
import { reopenTerminalAgentRun, resetAuthorizationSurface } from "../execution/agent-run-state.js";
|
|
20
21
|
import { ensurePermissionPlan, permissionPreflightLines, shouldNotifyPermissionPlan } from "../policy/permission-preflight.js";
|
|
21
22
|
import { buildTaskInferenceDirective } from "../cognition/task-inference-engine.js";
|
|
@@ -189,6 +190,13 @@ const ASKING_CMD_RE = /^(answer|resolve|contract|progress|task|skills|assumption
|
|
|
189
190
|
* @returns {Promise<void>}
|
|
190
191
|
*/
|
|
191
192
|
export async function handleMessage(input, output, deps) {
|
|
193
|
+
const turnStart = performance.now();
|
|
194
|
+
let phaseStart = turnStart;
|
|
195
|
+
const phase = (label) => {
|
|
196
|
+
const now = performance.now();
|
|
197
|
+
logger.info("turn-timing", `phase=${label} durMs=${(now - phaseStart).toFixed(1)} totalMs=${(now - turnStart).toFixed(1)}`);
|
|
198
|
+
phaseStart = now;
|
|
199
|
+
};
|
|
192
200
|
const {
|
|
193
201
|
collector = null,
|
|
194
202
|
bypassed,
|
|
@@ -220,7 +228,6 @@ export async function handleMessage(input, output, deps) {
|
|
|
220
228
|
persistLiveContext,
|
|
221
229
|
assembleContext,
|
|
222
230
|
selectContextWithCqe,
|
|
223
|
-
detectFileSearchIntent,
|
|
224
231
|
createSnapshot,
|
|
225
232
|
checkContinuation,
|
|
226
233
|
rebuildScope,
|
|
@@ -291,6 +298,7 @@ export async function handleMessage(input, output, deps) {
|
|
|
291
298
|
taskId = resolved.taskId;
|
|
292
299
|
fallbackKind = resolved.fallbackKind;
|
|
293
300
|
}
|
|
301
|
+
phase("resolveTask");
|
|
294
302
|
if (input.sessionID) {
|
|
295
303
|
sessionTasks.set(input.sessionID, taskId);
|
|
296
304
|
// Persist active-task pointer to disk so /wam contract approve (which
|
|
@@ -324,6 +332,7 @@ export async function handleMessage(input, output, deps) {
|
|
|
324
332
|
const existing = getTaskState(taskId, wamRoot);
|
|
325
333
|
if (existing) validateState(existing);
|
|
326
334
|
} catch {}
|
|
335
|
+
phase("loadTaskState");
|
|
327
336
|
try { pruneCompletedTasks(wamRoot).catch(() => {}); } catch {}
|
|
328
337
|
|
|
329
338
|
// Welcome / disambiguation gate: cuando la primera inferencia cayó en
|
|
@@ -607,6 +616,7 @@ export async function handleMessage(input, output, deps) {
|
|
|
607
616
|
}
|
|
608
617
|
|
|
609
618
|
const updatedState = getTaskState(taskId, wamRoot) || state;
|
|
619
|
+
if (intentLane.lane === INTENT_LANES.CORRECTION) updatedState.flow = "correction";
|
|
610
620
|
|
|
611
621
|
// Final completion message: emit once when the gate allows DONE.
|
|
612
622
|
if (
|
|
@@ -669,18 +679,20 @@ export async function handleMessage(input, output, deps) {
|
|
|
669
679
|
);
|
|
670
680
|
}
|
|
671
681
|
} catch {}
|
|
682
|
+
phase("history");
|
|
672
683
|
try {
|
|
673
684
|
const memory = loadConversationMemory(wamRoot, input.sessionID);
|
|
674
685
|
const memoryText = formatMemoryForContext(memory, promptText, 3);
|
|
675
686
|
if (memoryText) auxInject.push(`[wam memory]\n${memoryText}\n`);
|
|
676
687
|
} catch {}
|
|
688
|
+
phase("memory");
|
|
677
689
|
}
|
|
678
690
|
|
|
679
691
|
const reqs = updatedState.requirements || [];
|
|
680
692
|
const pend = reqs.filter((r) => r.status !== "done" && r.status !== "verified");
|
|
681
693
|
const objective = updatedState.contract?.objective || analysis.intent?.goal || null;
|
|
682
694
|
if (intentLane.lane !== INTENT_LANES.CONVERSATION) {
|
|
683
|
-
const requireApproval = cfg.contractApproval === "required";
|
|
695
|
+
const requireApproval = cfg.contractApproval === "required" && intentLane.lane !== INTENT_LANES.CORRECTION;
|
|
684
696
|
if (requireApproval && updatedState.contract?.status !== "APPROVED" && reqs.length > 0) {
|
|
685
697
|
const { visible, overflow } = splitVisibleRequirements(reqs);
|
|
686
698
|
const verification = (updatedState.contract?.verification || []).filter(
|
|
@@ -760,7 +772,7 @@ export async function handleMessage(input, output, deps) {
|
|
|
760
772
|
}
|
|
761
773
|
}
|
|
762
774
|
|
|
763
|
-
if (
|
|
775
|
+
if (cfg.disableCqe !== true) {
|
|
764
776
|
const cqeT0 = Date.now();
|
|
765
777
|
const cqePack = await selectContextWithCqe(promptText, {
|
|
766
778
|
root: wamRoot,
|
|
@@ -811,6 +823,7 @@ export async function handleMessage(input, output, deps) {
|
|
|
811
823
|
}
|
|
812
824
|
}
|
|
813
825
|
} catch {}
|
|
826
|
+
phase("cqe");
|
|
814
827
|
|
|
815
828
|
// Directive inference
|
|
816
829
|
if (updatedState.contract?.objective && updatedState.phase !== "DONE" && analysis.intent?.classification !== "trivial") {
|
|
@@ -874,6 +887,7 @@ const { lines } = buildTaskInferenceDirective({
|
|
|
874
887
|
}
|
|
875
888
|
updatedState.completionGateKey = gateKey;
|
|
876
889
|
persistTaskState(taskId, updatedState, wamRoot);
|
|
890
|
+
phase("persist");
|
|
877
891
|
}
|
|
878
892
|
// Mismo set de pendientes que el turno anterior: no repetir el aviso.
|
|
879
893
|
// -------------------------------------------------------------
|
|
@@ -966,6 +980,7 @@ const { lines } = buildTaskInferenceDirective({
|
|
|
966
980
|
emitTextPart(output, wamBlockText, { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
|
|
967
981
|
}
|
|
968
982
|
|
|
983
|
+
phase("rewrite");
|
|
969
984
|
input.waitAnalysis = analysis;
|
|
970
985
|
} catch (err) {
|
|
971
986
|
console.error("[wait-a-minute] Pre-flight analysis failed:", err);
|
|
@@ -170,6 +170,21 @@ function logDecision(event) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
const CORRECTION_FLOW_RE =
|
|
174
|
+
/\b(corrige|corregir|correcci[oó]n|arregla|arreglar|arreglo|fix|bugfix|bug|debug|falla|fallas|error|errores|regression|incorrecto|mal)\b/i;
|
|
175
|
+
|
|
176
|
+
export function isCorrectionFlow(state) {
|
|
177
|
+
if (!state) return false;
|
|
178
|
+
const flow = String(state.flow || "").toLowerCase();
|
|
179
|
+
if (flow === "correction") return true;
|
|
180
|
+
const kind = String(state.kind || state.contract?.kind || "").toLowerCase();
|
|
181
|
+
if (kind === "debugging" || kind === "bugfix") return true;
|
|
182
|
+
const text = [state.lastAction, state.contract?.objective, state.nextAction]
|
|
183
|
+
.filter((v) => typeof v === "string")
|
|
184
|
+
.join(" ");
|
|
185
|
+
return CORRECTION_FLOW_RE.test(text);
|
|
186
|
+
}
|
|
187
|
+
|
|
173
188
|
/**
|
|
174
189
|
* @param {string} tool
|
|
175
190
|
* @param {object|null} state - task state ({ phase, contract, requirements }).
|
|
@@ -211,6 +226,11 @@ export function enforceGovernance(tool, state, ctx = {}) {
|
|
|
211
226
|
const contractStatus = state?.contract?.status;
|
|
212
227
|
const phase = state?.phase;
|
|
213
228
|
|
|
229
|
+
if (isCorrectionFlow(state)) {
|
|
230
|
+
logDecision({ decision: "allow", reason: "correction-flow", tool, phase });
|
|
231
|
+
return { allowed: true, reason: "correction-flow" };
|
|
232
|
+
}
|
|
233
|
+
|
|
214
234
|
if (contractStatus === "APPROVED") return { allowed: true, reason: "contract-approved" };
|
|
215
235
|
if (isTerminalPhase(phase)) return { allowed: true, reason: "terminal-phase" };
|
|
216
236
|
// Let the Clarification Gate handle ASKING (answer the unknown, don't approve).
|
|
@@ -118,7 +118,7 @@ export function inferTaskKind(task = {}) {
|
|
|
118
118
|
|
|
119
119
|
if (!text) return TASK_KINDS.UNKNOWN;
|
|
120
120
|
if (/\brefactor/.test(text)) return TASK_KINDS.REFACTORING;
|
|
121
|
-
if (/\b(debug|bug|fix|failure|error|regression)\b/.test(text)) return TASK_KINDS.DEBUGGING;
|
|
121
|
+
if (/\b(debug|bug|bugfix|fix|failure|error|errores|regression|falla|fallas|corrige|corregir|correcci[oó]n|arregla|arreglar|arreglo)\b/.test(text)) return TASK_KINDS.DEBUGGING;
|
|
122
122
|
if (/\b(review|audit|inspect)\b/.test(text)) return TASK_KINDS.REVIEW;
|
|
123
123
|
if (/\b(document|documentation|readme|docs)\b/.test(text)) return TASK_KINDS.DOCUMENTATION;
|
|
124
124
|
if (/\b(research|investigate|explore|compare|evaluate|analysis)\b/.test(text)) return TASK_KINDS.RESEARCH;
|