opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.8
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/LICENSE +21 -0
- package/README.md +318 -2
- package/index.js +1836 -0
- package/package.json +70 -4
- package/skills/codebase-design/SKILL.md +114 -0
- package/skills/design-contract/SKILL.md +27 -0
- package/skills/design-critique/SKILL.md +19 -0
- package/skills/design-system/SKILL.md +17 -0
- package/skills/design-system/references/image-analysis.md +14 -0
- package/skills/design-system/references/tokens.md +32 -0
- package/skills/design-system/references/ui-rules.md +14 -0
- package/skills/diagnosing-bugs/SKILL.md +138 -0
- package/skills/frontend-design/SKILL.md +36 -0
- package/skills/handoff/SKILL.md +16 -0
- package/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/skills/linter-negative/SKILL.md +74 -0
- package/skills/no-slop-ui/SKILL.md +26 -0
- package/skills/prototype/SKILL.md +26 -0
- package/skills/registry.json +44087 -0
- package/skills/tastemaker/SKILL.md +8 -0
- package/skills/tastemaker/references/tastemaker.md +14 -0
- package/skills/tdd/SKILL.md +38 -0
- package/skills/visual-review/SKILL.md +103 -0
- package/skills/visual-review/references/review-gates.md +42 -0
- package/skills/visual-review/references/surface-archetypes.md +37 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/writing-for-agents/SKILL.md +81 -0
- package/src/cognition/cognition-store.js +251 -0
- package/src/cognition/cognitive-state.js +126 -0
- package/src/cognition/hypothesis-manager.js +113 -0
- package/src/cognition/observation-engine.js +112 -0
- package/src/cognition/observation-provenance.js +268 -0
- package/src/context/active-context-boundary.js +68 -0
- package/src/context/assembly.js +494 -0
- package/src/context/assessment-rules.js +55 -0
- package/src/context/capsule-staleness.js +267 -0
- package/src/context/capsules.js +2 -0
- package/src/context/compare-array.js +98 -0
- package/src/context/compare-object.js +89 -0
- package/src/context/compare-values.js +42 -0
- package/src/context/context-assembly-contract.js +71 -0
- package/src/context/context-budget-manager.js +232 -0
- package/src/context/context-builder.js +49 -0
- package/src/context/context-cache.js +55 -0
- package/src/context/context-capture.js +243 -0
- package/src/context/context-classification.js +199 -0
- package/src/context/context-compaction.js +179 -0
- package/src/context/context-decision-audit.js +267 -0
- package/src/context/context-demotion.js +173 -0
- package/src/context/context-dictionary.js +106 -0
- package/src/context/context-drift-detection.js +225 -0
- package/src/context/context-evaluation.js +374 -0
- package/src/context/context-event-ingress.js +100 -0
- package/src/context/context-event-normalization.js +224 -0
- package/src/context/context-feedback-loop.js +216 -0
- package/src/context/context-freshness.js +21 -0
- package/src/context/context-graph-builder.js +71 -0
- package/src/context/context-graph.js +451 -0
- package/src/context/context-history-relevance.js +321 -0
- package/src/context/context-interception.js +145 -0
- package/src/context/context-invalidation-events.js +154 -0
- package/src/context/context-lifecycle.js +170 -0
- package/src/context/context-loop-prevention.js +347 -0
- package/src/context/context-manager.js +340 -0
- package/src/context/context-memory-layers.js +265 -0
- package/src/context/context-minimality.js +166 -0
- package/src/context/context-normalization.js +130 -0
- package/src/context/context-optimization-metrics.js +434 -0
- package/src/context/context-output-policy.js +135 -0
- package/src/context/context-pack-authority.js +157 -0
- package/src/context/context-promotion.js +172 -0
- package/src/context/context-query-contract.js +101 -0
- package/src/context/context-ranking.js +164 -0
- package/src/context/context-rate-distortion.js +261 -0
- package/src/context/context-recovery.js +209 -0
- package/src/context/context-retrieval-integration.js +60 -0
- package/src/context/context-retrieval.js +93 -0
- package/src/context/context-router.js +457 -0
- package/src/context/context-routing-evaluation.js +331 -0
- package/src/context/context-routing-observability.js +296 -0
- package/src/context/context-routing-shadow.js +237 -0
- package/src/context/context-scope.js +181 -0
- package/src/context/context-snapshot.js +226 -0
- package/src/context/context-source-registry.js +95 -0
- package/src/context/context-sufficiency-gate.js +120 -0
- package/src/context/context-sufficiency-oracle.js +167 -0
- package/src/context/context-telemetry.js +176 -0
- package/src/context/context.js +633 -0
- package/src/context/dependency-closure.js +246 -0
- package/src/context/page-fault-tracker.js +275 -0
- package/src/context/prompt-engine.js +74 -0
- package/src/context/runtime-context-graph.js +260 -0
- package/src/context/selection.js +2 -0
- package/src/context/session.js +2 -0
- package/src/context/sufficiency-contract.js +300 -0
- package/src/context/tokenizer.js +17 -0
- package/src/context-benchmark-router.mjs +125 -0
- package/src/evidence/evidence-engine.js +161 -0
- package/src/evidence/evidence-factory.js +3 -0
- package/src/evidence/evidence-freshness.js +463 -0
- package/src/evidence/evidence-gap.js +76 -0
- package/src/evidence/evidence-lineage.js +525 -0
- package/src/evidence/evidence.js +136 -0
- package/src/evidence/freshness-validation.js +187 -0
- package/src/evidence/lineage.js +4 -0
- package/src/execution/action-evaluation.js +179 -0
- package/src/execution/circuit-breaker.js +70 -0
- package/src/execution/claim-interception.js +63 -0
- package/src/execution/execution-engine.js +258 -0
- package/src/execution/execution-intent.js +69 -0
- package/src/execution/execution-state.js +89 -0
- package/src/execution/formatting.js +60 -0
- package/src/execution/l1-sanitize.js +16 -0
- package/src/execution/l2-resource-guards.js +19 -0
- package/src/execution/l3-invariant-checker.js +5 -0
- package/src/execution/l4-containment.js +6 -0
- package/src/execution/runtime-guards.js +106 -0
- package/src/execution/strategy-identity.js +162 -0
- package/src/execution/tool-interceptor.js +7 -0
- package/src/execution/tool-result-classifier.js +63 -0
- package/src/integration/canary-deploy.js +143 -0
- package/src/integration/collector.js +59 -0
- package/src/integration/error-recovery.js +6 -0
- package/src/integration/event-bus.js +66 -0
- package/src/integration/health-checker.js +57 -0
- package/src/integration/logger.js +39 -0
- package/src/integration/message-handler.js +611 -0
- package/src/integration/opencode-db.js +153 -0
- package/src/integration/orchestration.js +419 -0
- package/src/integration/part-provenance.js +65 -0
- package/src/integration/rate-limiter.js +55 -0
- package/src/integration/release-gate.js +336 -0
- package/src/integration/rollback-manager.js +164 -0
- package/src/integration/router-adapter.js +269 -0
- package/src/integration/telemetry.js +45 -0
- package/src/integration/update-checker.js +10 -0
- package/src/persistence/event-sourcing.js +75 -0
- package/src/persistence/memory.js +501 -0
- package/src/persistence/migration-runner.js +16 -0
- package/src/persistence/persistence-manager.js +39 -0
- package/src/persistence/transaction-log.js +157 -0
- package/src/policy/action-capabilities.js +116 -0
- package/src/policy/action-policy.js +4 -0
- package/src/policy/assessment-engine.js +104 -0
- package/src/policy/assumption-tracking.js +66 -0
- package/src/policy/budget-enforcer.js +7 -0
- package/src/policy/config-validator.js +102 -0
- package/src/policy/execution-assessment.js +142 -0
- package/src/policy/failure-classification.js +5 -0
- package/src/policy/feature-flags.js +65 -0
- package/src/policy/governance-enforcement.js +118 -0
- package/src/policy/locale.js +15 -0
- package/src/policy/multi-signer.js +151 -0
- package/src/policy/policy-state-machine.js +97 -0
- package/src/policy/ponytail-constraint.js +38 -0
- package/src/policy/project-inspector.js +93 -0
- package/src/policy/query-builder.js +14 -0
- package/src/policy/request-classifier.js +79 -0
- package/src/policy/risk-engine.js +266 -0
- package/src/policy/scope-enforcement.js +140 -0
- package/src/policy/secret-scanner.js +68 -0
- package/src/policy/skill-routing.js +104 -0
- package/src/policy/strategy-capabilities.js +211 -0
- package/src/policy/strategy.js +6 -0
- package/src/policy/uncertainty.js +117 -0
- package/src/shared/container.js +44 -0
- package/src/shared/decision-types.js +24 -0
- package/src/shared/simple-test.js +160 -0
- package/src/shared/wam-events.js +52 -0
- package/src/skills/engine.js +1705 -0
- package/src/skills/skill-registry.js +19 -0
- package/src/skills/skill-routing.js +340 -0
- package/src/state/lifecycle-manager.js +113 -0
- package/src/state/operational-state.js +198 -0
- package/src/state/projections.js +47 -0
- package/src/state/replay-engine.js +5 -0
- package/src/state/requirement-ownership.js +20 -0
- package/src/state/requirement-state.js +152 -0
- package/src/state/run-event-identity.js +172 -0
- package/src/state/state-engine.js +19 -0
- package/src/state/state-machine.js +119 -0
- package/src/state/state-schema.js +318 -0
- package/src/state/state-store.js +335 -0
- package/src/state/state-transition.js +84 -0
- package/src/state/task-dependencies.js +287 -0
- package/src/state/task-execution.js +217 -0
- package/src/state/task-lifecycle.js +59 -0
- package/src/state/task-runs.js +361 -0
- package/src/state/task-state-file.js +86 -0
- package/src/state/task-state.js +41 -0
- package/src/state/task-status.js +54 -0
- package/src/state/task-store.js +48 -0
- package/src/state/wam-state.js +148 -0
- package/src/verification/completion-gate-simple.js +8 -0
- package/src/verification/completion-gate.js +183 -0
- package/src/verification/contract.js +9 -0
- package/src/verification/contradiction-resolution.js +255 -0
- package/src/verification/diagnostic-engine.js +659 -0
- package/src/verification/false-completion-prevention.js +100 -0
- package/src/verification/hook-validator.js +6 -0
- package/src/verification/requirements.js +6 -0
- package/src/verification/verification-context.js +162 -0
- package/src/verification/verification-lifecycle.js +66 -0
- package/src/verification/verification-model.js +89 -0
- package/src/verification/verification-persistence.js +129 -0
- package/src/verification/verification-policy.js +127 -0
- package/src/verification/verification.js +415 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime message handler — chat.message hook logic.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.js to modularize the message processing pipeline.
|
|
5
|
+
* Receives all dependencies via a `deps` object (inversion of control),
|
|
6
|
+
* making it independently testable.
|
|
7
|
+
*
|
|
8
|
+
* Usage from index.js:
|
|
9
|
+
* "chat.message": (input, output) => handleMessage(input, output, deps),
|
|
10
|
+
*
|
|
11
|
+
* where `deps` contains everything the handler needs (see DEPENDENCIES
|
|
12
|
+
* section in the typedef below for the full list).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
import { hasWamMarkerFor, isWamSynthetic } from "./part-provenance.js";
|
|
19
|
+
|
|
20
|
+
// Idempotency guard: a chat.message invocation must be processed at most once
|
|
21
|
+
// per message id + kind. The store is normally owned by the plugin instance
|
|
22
|
+
// (deps.processedMessages); the module-level fallback keeps direct callers and
|
|
23
|
+
// tests working. Bounded so a long session cannot grow unbounded.
|
|
24
|
+
const PROCESSED_MESSAGE_LIMIT = 1000;
|
|
25
|
+
const fallbackProcessedMessages = new Map();
|
|
26
|
+
|
|
27
|
+
function markProcessed(key, store = fallbackProcessedMessages) {
|
|
28
|
+
store.set(key, true);
|
|
29
|
+
if (store.size > PROCESSED_MESSAGE_LIMIT) {
|
|
30
|
+
const oldest = store.keys().next().value;
|
|
31
|
+
if (oldest !== undefined) store.delete(oldest);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function _resetProcessedMessages(store = fallbackProcessedMessages) {
|
|
36
|
+
store.clear();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Pure predicate: true when this message was already handled by WAM.
|
|
41
|
+
* Serves as the runtime guard and as a testable unit.
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} input
|
|
44
|
+
* @param {Object} output
|
|
45
|
+
* @param {Map} [store]
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
export function isMessageAlreadyProcessed(input, output, store = fallbackProcessedMessages) {
|
|
49
|
+
const messageId = input?.messageID || output?.message?.id || input?.message?.id || null;
|
|
50
|
+
if (!messageId) return false;
|
|
51
|
+
const key = `${messageId}:${input?.tool ? "tool" : "prompt"}`;
|
|
52
|
+
return (
|
|
53
|
+
store.has(key) ||
|
|
54
|
+
hasWamMarkerFor(output?.parts, messageId) ||
|
|
55
|
+
hasWamMarkerFor(input?.message?.parts, messageId)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Generic/new task ids must never be treated as explicit task identity.
|
|
60
|
+
// Kept in sync with index.js GENERIC_TASK.
|
|
61
|
+
const GENERIC_TASK = /^(default-task|task|general|)$/;
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// CONSTANTS
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
const RESUME_RE = /\b(en qué estábamos|en que estabamos|dónde íbamos|donde íbamos|sigamos|continuemos|retomar la tarea)\b/i;
|
|
68
|
+
const MAX_VISIBLE_REQS = 5;
|
|
69
|
+
const ASKING_CMD_RE = /^(answer|resolve|contract|progress|task|skills|assumptions|compress)\b/;
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// DEPENDENCIES (provided by caller via deps parameter)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @typedef {Object} MessageHandlerDeps
|
|
77
|
+
* @property {boolean} bypassed
|
|
78
|
+
* @property {Map} sessionTasks
|
|
79
|
+
* @property {Map} sessionStore
|
|
80
|
+
* @property {Object} cfg
|
|
81
|
+
* @property {Function} resolveSessionBase
|
|
82
|
+
* @property {Function} wamRootFor
|
|
83
|
+
* @property {Function} ensureWamMemory
|
|
84
|
+
* @property {Function} effectiveTaskId
|
|
85
|
+
* @property {Function} genPartId
|
|
86
|
+
* @property {Map} [processedMessages]
|
|
87
|
+
* @property {Function} emitTextPart
|
|
88
|
+
* @property {Function} readActiveTaskIdFresh
|
|
89
|
+
* @property {Function} writeActiveTaskId
|
|
90
|
+
* @property {Object} waitAMinute
|
|
91
|
+
* @property {Function} migrateLegacyCognition
|
|
92
|
+
* @property {Function} noteSuccess
|
|
93
|
+
* @property {Function} noteFailure
|
|
94
|
+
* @property {Function} getTaskState
|
|
95
|
+
* @property {Function} persistTaskState
|
|
96
|
+
* @property {Function} findDuplicateTask
|
|
97
|
+
* @property {Function} escalateAssumptions
|
|
98
|
+
* @property {Function} buildAssumptions
|
|
99
|
+
* @property {Function} initMemory
|
|
100
|
+
* @property {Function} updateProjectMemo
|
|
101
|
+
* @property {Function} updateLiveContext
|
|
102
|
+
* @property {Function} readLiveContext
|
|
103
|
+
* @property {Function} persistLiveContext
|
|
104
|
+
* @property {Function} assembleContext
|
|
105
|
+
* @property {Function} createSnapshot
|
|
106
|
+
* @property {Function} checkContinuation
|
|
107
|
+
* @property {Function} rebuildScope
|
|
108
|
+
* @property {Function} getDecision
|
|
109
|
+
* @property {Function} recordDecision
|
|
110
|
+
* @property {Function} compactDecisions
|
|
111
|
+
* @property {Function} writeCavemanSummary
|
|
112
|
+
* @property {Function} truncate
|
|
113
|
+
* @property {Function} delegationLines
|
|
114
|
+
* @property {Function} updateTaskMemory
|
|
115
|
+
* @property {Function} addRecentChange
|
|
116
|
+
* @property {Function} closeSession
|
|
117
|
+
* @property {Function} getSessionId
|
|
118
|
+
* @property {Function} classifyAskingMessage
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Handles the chat.message hook: deterministic task-state and context analysis of user messages.
|
|
123
|
+
*
|
|
124
|
+
* @param {Object} input - OpenCode chat.message input
|
|
125
|
+
* @param {Object} output - OpenCode chat.message output (mutated in place)
|
|
126
|
+
* @param {MessageHandlerDeps} deps
|
|
127
|
+
* @returns {Promise<void>}
|
|
128
|
+
*/
|
|
129
|
+
export async function handleMessage(input, output, deps) {
|
|
130
|
+
const {
|
|
131
|
+
collector = null,
|
|
132
|
+
bypassed,
|
|
133
|
+
sessionTasks,
|
|
134
|
+
sessionStore,
|
|
135
|
+
cfg,
|
|
136
|
+
resolveSessionBase,
|
|
137
|
+
wamRootFor,
|
|
138
|
+
ensureWamMemory,
|
|
139
|
+
effectiveTaskId,
|
|
140
|
+
genPartId,
|
|
141
|
+
emitTextPart,
|
|
142
|
+
readActiveTaskIdFresh,
|
|
143
|
+
writeActiveTaskId,
|
|
144
|
+
waitAMinute,
|
|
145
|
+
migrateLegacyCognition,
|
|
146
|
+
noteSuccess,
|
|
147
|
+
noteFailure,
|
|
148
|
+
getTaskState,
|
|
149
|
+
persistTaskState,
|
|
150
|
+
findDuplicateTask,
|
|
151
|
+
escalateAssumptions,
|
|
152
|
+
buildAssumptions,
|
|
153
|
+
initMemory,
|
|
154
|
+
updateProjectMemo,
|
|
155
|
+
updateLiveContext,
|
|
156
|
+
readLiveContext,
|
|
157
|
+
persistLiveContext,
|
|
158
|
+
assembleContext,
|
|
159
|
+
createSnapshot,
|
|
160
|
+
checkContinuation,
|
|
161
|
+
rebuildScope,
|
|
162
|
+
getDecision,
|
|
163
|
+
recordDecision,
|
|
164
|
+
compactDecisions,
|
|
165
|
+
writeCavemanSummary,
|
|
166
|
+
truncate,
|
|
167
|
+
delegationLines,
|
|
168
|
+
updateTaskMemory,
|
|
169
|
+
addRecentChange,
|
|
170
|
+
closeSession,
|
|
171
|
+
getSessionId,
|
|
172
|
+
classifyAskingMessage,
|
|
173
|
+
} = deps;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
if (bypassed) return;
|
|
177
|
+
|
|
178
|
+
// Idempotency: never process the same message twice. WAM injects synthetic
|
|
179
|
+
// parts into `output`; without this guard a re-invocation (retry/replay)
|
|
180
|
+
// would read its own injection as a fresh prompt and loop indefinitely.
|
|
181
|
+
const processedStore = deps.processedMessages || fallbackProcessedMessages;
|
|
182
|
+
if (isMessageAlreadyProcessed(input, output, processedStore)) return;
|
|
183
|
+
{
|
|
184
|
+
const messageId = input?.messageID || output?.message?.id || input?.message?.id || null;
|
|
185
|
+
if (messageId) markProcessed(`${messageId}:${input?.tool ? "tool" : "prompt"}`, processedStore);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const promptText = extractPrompt(input, output);
|
|
189
|
+
if (!promptText.trim()) return;
|
|
190
|
+
|
|
191
|
+
// --- Tool execution integration (passive state preservation) ---
|
|
192
|
+
if (input?.tool && output?.result !== undefined) {
|
|
193
|
+
const toolName = input.tool;
|
|
194
|
+
const wamRoot = await ensureWamMemory(input.sessionID, promptText);
|
|
195
|
+
const taskId = effectiveTaskId(input, sessionTasks, wamRoot);
|
|
196
|
+
|
|
197
|
+
try { migrateLegacyCognition(wamRoot, taskId); } catch (e) { console.log(`[wait-a-minute] migration error:`, e.message); }
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await noteSuccess(wamRoot, taskId, {
|
|
201
|
+
hypothesisId: input._wamHypothesisId || input.hypothesisId,
|
|
202
|
+
experimentId: input._wamExperimentId || input.experimentId,
|
|
203
|
+
result: output.result,
|
|
204
|
+
actual: output.actual,
|
|
205
|
+
unexpected: output.unexpected,
|
|
206
|
+
provenance: `agent-tool-${toolName}-success`,
|
|
207
|
+
requirementId: input._wamRequirementId || input.requirementId,
|
|
208
|
+
});
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.log(`[wait-a-minute] noteSuccess integration error:`, e.message);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (input?.tool && output?.error) {
|
|
215
|
+
const toolName = input.tool;
|
|
216
|
+
const wamRoot = await ensureWamMemory(input.sessionID, promptText);
|
|
217
|
+
const taskId = effectiveTaskId(input, sessionTasks, wamRoot);
|
|
218
|
+
|
|
219
|
+
try { migrateLegacyCognition(wamRoot, taskId); } catch (e) { console.log(`[wait-a-minute] migration error:`, e.message); }
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
await noteFailure(wamRoot, taskId, {
|
|
223
|
+
hypothesisId: input._wamHypothesisId || input.hypothesisId,
|
|
224
|
+
experimentId: input._wamExperimentId || input.experimentId,
|
|
225
|
+
reason: output.error || "tool execution failed",
|
|
226
|
+
actual: output.actual,
|
|
227
|
+
unexpected: output.unexpected,
|
|
228
|
+
provenance: `agent-tool-${toolName}-failure`,
|
|
229
|
+
});
|
|
230
|
+
} catch (e) {
|
|
231
|
+
console.log(`[wait-a-minute] noteFailure integration error:`, e.message);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!promptText.trim()) return;
|
|
236
|
+
|
|
237
|
+
// Ensure WAM memory for this session
|
|
238
|
+
const wamRoot = await ensureWamMemory(input.sessionID, promptText);
|
|
239
|
+
|
|
240
|
+
// No-task-assumption: resume intent without active task → ask, don't assume
|
|
241
|
+
if (!input.taskId && RESUME_RE.test(promptText)) {
|
|
242
|
+
const active = sessionTasks.get(input.sessionID) || readActiveTaskIdFresh(wamRoot);
|
|
243
|
+
const st = active ? getTaskState(active, wamRoot) : null;
|
|
244
|
+
if (st && st.phase !== "DONE") {
|
|
245
|
+
emitTextPart(output, `[wait-a-minute] Hay una tarea pendiente: ${active} (fase ${st.phase}). ¿Quieres continuarla? Responde /wam resume ${active} o define una tarea nueva — no asumo intención.`, { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
|
|
246
|
+
} else {
|
|
247
|
+
emitTextPart(output, "[wait-a-minute] No hay tarea activa. Dime qué tarea nueva quieres — no asumo intención previa.", { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Effective taskId with session persistence
|
|
253
|
+
let taskId = effectiveTaskId(input, sessionTasks, wamRoot);
|
|
254
|
+
if (input.sessionID) sessionTasks.set(input.sessionID, taskId);
|
|
255
|
+
|
|
256
|
+
// Clarification Gate: in ASKING classify the message
|
|
257
|
+
const askingState = getTaskState(taskId, wamRoot);
|
|
258
|
+
if (askingState?.phase === "ASKING") {
|
|
259
|
+
const trimmed = promptText.trim();
|
|
260
|
+
if (ASKING_CMD_RE.test(trimmed)) return; // handled by command.execute.before
|
|
261
|
+
const kind = classifyAskingMessage(trimmed);
|
|
262
|
+
if (kind === "blocked-message") {
|
|
263
|
+
const u = (askingState.contract?.unknowns || []).find((x) => x.status === "blocking");
|
|
264
|
+
const directive = u
|
|
265
|
+
? `⛔ [wait-a-minute] BLOQUEADO: Pregunta pendiente — ${u.id}: ${u.question}\nResponder: /wam answer ${u.id} <respuesta>`
|
|
266
|
+
: "⛔ [wait-a-minute] BLOQUEADO: Tienes preguntas sin responder.";
|
|
267
|
+
emitTextPart(output, directive, { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
|
|
268
|
+
// Rewrite the original prompt so the implementation attempt is NOT
|
|
269
|
+
// consumed downstream as an instruction (AC5).
|
|
270
|
+
const rewrite = `[wait-a-minute] BLOQUEADO — prompt de implementación no consumido como respuesta. ${directive}`;
|
|
271
|
+
if (input?.message?.parts && input.message.parts.length > 0) {
|
|
272
|
+
const tp = input.message.parts.find((p) => p.type === "text" && typeof p.text === "string");
|
|
273
|
+
if (tp) tp.text = rewrite;
|
|
274
|
+
}
|
|
275
|
+
if (input?.parts && input.parts.length > 0) {
|
|
276
|
+
const tp = input.parts.find((p) => p.type === "text" && typeof p.text === "string");
|
|
277
|
+
if (tp) tp.text = rewrite;
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (kind === "new-intent") {
|
|
282
|
+
emitTextPart(output, "[wait-a-minute] Nueva intención detectada. Abriendo tarea nueva.", { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
|
|
283
|
+
// Override the explicit taskId so subsequent analysis creates a fresh
|
|
284
|
+
// task (task-<ts>) without contaminating unknowns from the previous task.
|
|
285
|
+
const newTaskId = `task-${Date.now()}`;
|
|
286
|
+
if (input) input.taskId = newTaskId;
|
|
287
|
+
try {
|
|
288
|
+
if (input?.sessionID) sessionTasks.set(input.sessionID, newTaskId);
|
|
289
|
+
writeActiveTaskId(newTaskId, wamRoot);
|
|
290
|
+
} catch {}
|
|
291
|
+
// Update local taskId so the rest of handleMessage uses the new task.
|
|
292
|
+
taskId = newTaskId;
|
|
293
|
+
// Fall through to normal analysis: a new task-<ts> task gets created.
|
|
294
|
+
}
|
|
295
|
+
// answer: in ASKING a natural-language message resolves the blocking question.
|
|
296
|
+
const ansRes = waitAMinute.answerFromMessage(taskId, trimmed, wamRoot);
|
|
297
|
+
if (ansRes && ansRes.ok) {
|
|
298
|
+
const u = ansRes.u;
|
|
299
|
+
emitTextPart(
|
|
300
|
+
output,
|
|
301
|
+
`[wait-a-minute] question answered — ${u?.id || "U1"}: ${u?.answer || trimmed}. Ready → Proceed (${u?.question || "pregunta resuelta"})`,
|
|
302
|
+
{ sessionID: input.sessionID, messageID: output.message?.id || input.messageID },
|
|
303
|
+
);
|
|
304
|
+
// After resolving, fall through to re-analysis so contract is rebuilt.
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Project scope change → backlog + new task
|
|
309
|
+
const existingState = getTaskState(taskId, wamRoot);
|
|
310
|
+
if (existingState?.projectPath && existingState.projectPath !== wamRoot) {
|
|
311
|
+
waitAMinute.addToBacklog(
|
|
312
|
+
existingState,
|
|
313
|
+
`Task ${taskId}: ${existingState.contract?.requirements?.map((r) => r.title).join("; ") || "sin reqs"}`,
|
|
314
|
+
"project-switch",
|
|
315
|
+
);
|
|
316
|
+
persistTaskState(taskId, existingState, wamRoot);
|
|
317
|
+
try { fs.rmSync(path.join(wamRoot, ".wam", "active-task"), { force: true }); } catch {}
|
|
318
|
+
taskId = `task-${Date.now()}`;
|
|
319
|
+
input.taskId = taskId;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Continuation fast-path: approved contract + no DONE claim
|
|
323
|
+
if (existingState?.contract?.status === "APPROVED") {
|
|
324
|
+
const claim = waitAMinute.evaluateCompletionGate(existingState, promptText);
|
|
325
|
+
if (!claim.blocked && !claim.allDone) {
|
|
326
|
+
const snapshotCheck = checkContinuation(taskId, existingState, wamRoot);
|
|
327
|
+
|
|
328
|
+
if (snapshotCheck.status === "VALID") {
|
|
329
|
+
if (collector) collector.record("Context_fast_path");
|
|
330
|
+
input.waitAnalysis = sessionStore.get("waitAnalysis") || null;
|
|
331
|
+
try { updateProjectMemo({}, wamRoot); } catch {}
|
|
332
|
+
try {
|
|
333
|
+
persistLiveContext(taskId, existingState, wamRoot);
|
|
334
|
+
const live = readLiveContext(wamRoot, taskId);
|
|
335
|
+
if (live) {
|
|
336
|
+
const parts = [`[wam N2 task]\n${live}\n`];
|
|
337
|
+
parts.push(...delegationLines(existingState).map((l) => l + "\n"));
|
|
338
|
+
emitTextPart(output, parts.join("\n"), { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
|
|
339
|
+
}
|
|
340
|
+
} catch {}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const scope = rebuildScope(snapshotCheck.changedSignals, collector);
|
|
345
|
+
if (scope.rebuildN1 || scope.rebuildN3) {
|
|
346
|
+
input.waitAnalysis = sessionStore.get("waitAnalysis") || null;
|
|
347
|
+
try { updateProjectMemo({}, wamRoot); } catch {}
|
|
348
|
+
try {
|
|
349
|
+
persistLiveContext(taskId, existingState, wamRoot);
|
|
350
|
+
const live = readLiveContext(wamRoot, taskId);
|
|
351
|
+
if (live) {
|
|
352
|
+
const parts = [
|
|
353
|
+
`[wam continuation] Contexto reconstruido (${snapshotCheck.changedSignals.join(", ")})`,
|
|
354
|
+
`[wam N2 task]\n${live}\n`,
|
|
355
|
+
];
|
|
356
|
+
parts.push(...delegationLines(existingState).map((l) => l + "\n"));
|
|
357
|
+
emitTextPart(output, parts.join("\n"), { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
|
|
358
|
+
}
|
|
359
|
+
} catch {}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// INVALID: full rebuild → fall through to normal analyze()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ---- Normal analysis flow ----
|
|
367
|
+
const analysis = await waitAMinute.analyze({
|
|
368
|
+
prompt: promptText,
|
|
369
|
+
projectPath: wamRoot,
|
|
370
|
+
config: cfg,
|
|
371
|
+
tierCaps: cfg.tierCaps,
|
|
372
|
+
activePreset: cfg.activePreset,
|
|
373
|
+
activeMode: cfg.activeMode,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const state = waitAMinute.buildPersistedState(taskId, analysis, wamRoot);
|
|
377
|
+
state.lastAction = promptText;
|
|
378
|
+
|
|
379
|
+
// Task Dedup - CANDIDATE ONLY: never overrides taskId implicitly.
|
|
380
|
+
// - If input.taskId is explicit → NEVER override; do NOT even consult findDuplicateTask
|
|
381
|
+
// - If taskId is generic/new → findDuplicateTask may return a candidate
|
|
382
|
+
// but is reported, not assigned; caller decides via /wam task switch
|
|
383
|
+
const isExplicit = input?.taskId && !GENERIC_TASK.test(input.taskId);
|
|
384
|
+
if (!isExplicit) {
|
|
385
|
+
const existingTaskId = findDuplicateTask(promptText, wamRoot);
|
|
386
|
+
if (existingTaskId && existingTaskId !== taskId) {
|
|
387
|
+
// Candidate-only: report, do not reassign. The session continues with
|
|
388
|
+
// the freshly-initialized taskId; the user can reuse via /wam task switch.
|
|
389
|
+
emitTextPart(output, `[wait-a-minute] Tarea similar detectada: ${existingTaskId} (usa /wam task switch ${existingTaskId} para retomar)`, { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Assumption Gate
|
|
394
|
+
try {
|
|
395
|
+
const { changed } = escalateAssumptions(state, promptText);
|
|
396
|
+
if (changed) persistTaskState(taskId, state, wamRoot);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
console.error("[wait-a-minute] escalate assumptions failed:", err);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Store analysis in session
|
|
402
|
+
sessionStore.set("waitAnalysis", analysis);
|
|
403
|
+
sessionStore.set("completionContract", state.contract);
|
|
404
|
+
sessionStore.set("persistentPolicies", analysis.persistentPolicies || []);
|
|
405
|
+
sessionStore.set("skillRegistry", analysis.skillRegistry || {});
|
|
406
|
+
|
|
407
|
+
// Blocking questions → ASKING
|
|
408
|
+
const blockingUnknowns = (state.contract?.unknowns || []).filter((u) => u.status === "blocking");
|
|
409
|
+
const finalQuestions = blockingUnknowns.filter((u) => {
|
|
410
|
+
const decision = getDecision(u.id, wamRoot);
|
|
411
|
+
if (decision) { console.log(`[wait-a-minute] Found existing decision for ${u.id}: ${decision.decision}`); return false; }
|
|
412
|
+
return true;
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
if (finalQuestions.length > 0 && state.phase !== "ANSWERED") {
|
|
416
|
+
state.phase = "ASKING";
|
|
417
|
+
state.nextAction = "Responder pregunta bloqueante antes de ejecutar";
|
|
418
|
+
persistTaskState(taskId, state, wamRoot);
|
|
419
|
+
const questions = [];
|
|
420
|
+
for (const u of finalQuestions) {
|
|
421
|
+
questions.push(`⛔ [wait-a-minute] ASKING — ${u.id}: ${u.question}\nNo implementar hasta responder. Responder: /wam answer ${u.id} <respuesta>`);
|
|
422
|
+
}
|
|
423
|
+
emitTextPart(output, questions.join("\n\n"), { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Approval gate
|
|
428
|
+
const intentConf = Number(analysis.intent?.confidence ?? analysis.intent?.conf ?? 100);
|
|
429
|
+
const openUnknowns = (state.contract?.unknowns || []).filter((u) => u.status !== "answered" && u.status !== "blocking");
|
|
430
|
+
const highUncertainty =
|
|
431
|
+
(analysis.ambiguity || "low") === "high" ||
|
|
432
|
+
intentConf < 60 ||
|
|
433
|
+
openUnknowns.length > 0;
|
|
434
|
+
const trimmed = promptText.trim();
|
|
435
|
+
const userConfirms =
|
|
436
|
+
!highUncertainty ||
|
|
437
|
+
/^(s[ií]|ok|okey|dale|hazlo|adelante|continuar|continua|aprobar|confirmo|correcto|perfecto|bueno|va|listo|sigue)\b/i.test(trimmed) ||
|
|
438
|
+
trimmed.includes("aprobar contrato");
|
|
439
|
+
|
|
440
|
+
if (state.contract?.status === "PROPOSED" && state.phase === "PROPOSED" && userConfirms) {
|
|
441
|
+
waitAMinute.approveContract(taskId, wamRoot);
|
|
442
|
+
try {
|
|
443
|
+
recordDecision({
|
|
444
|
+
id: `strategy-${taskId}-${Date.now()}`,
|
|
445
|
+
decision: `Aprobar estrategia ${analysis.strategy || "NORMAL"} para ${taskId}`,
|
|
446
|
+
reason: promptText.slice(0, 120),
|
|
447
|
+
source: highUncertainty ? "user-decided" : "observed",
|
|
448
|
+
confidence: "high",
|
|
449
|
+
}, wamRoot);
|
|
450
|
+
try { compactDecisions(wamRoot); } catch {}
|
|
451
|
+
} catch {}
|
|
452
|
+
const fresh = getTaskState(taskId, wamRoot);
|
|
453
|
+
if (fresh) {
|
|
454
|
+
state.contract = fresh.contract;
|
|
455
|
+
state.phase = fresh.phase;
|
|
456
|
+
state.nextAction = fresh.nextAction;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const gate = waitAMinute.evaluateCompletionGate(state, promptText);
|
|
461
|
+
const { phase, nextAction } = waitAMinute.applyPhaseTransition(state, gate);
|
|
462
|
+
if (gate.blocked || gate.allDone) {
|
|
463
|
+
state.phase = phase;
|
|
464
|
+
state.nextAction = nextAction;
|
|
465
|
+
persistTaskState(taskId, state, wamRoot);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const updatedState = getTaskState(taskId, wamRoot);
|
|
469
|
+
|
|
470
|
+
// Live context snapshot
|
|
471
|
+
try { persistLiveContext(taskId, updatedState, wamRoot); } catch {}
|
|
472
|
+
|
|
473
|
+
// ---- Context Assembly ----
|
|
474
|
+
const inject = [];
|
|
475
|
+
|
|
476
|
+
const reqs = updatedState.requirements || [];
|
|
477
|
+
const pend = reqs.filter((r) => r.status !== "done");
|
|
478
|
+
const objective = updatedState.contract?.objective || analysis.intent?.goal || null;
|
|
479
|
+
if (!(analysis.intent?.classification === "trivial" || analysis.strategy === "FAST")) {
|
|
480
|
+
if (updatedState.phase === "PROPOSED" && !updatedState.contractDisplayed) {
|
|
481
|
+
const visible = reqs.slice(0, MAX_VISIBLE_REQS);
|
|
482
|
+
const overflow = reqs.length - MAX_VISIBLE_REQS;
|
|
483
|
+
inject.push(
|
|
484
|
+
`Objective: ${objective || "pending"}`,
|
|
485
|
+
`Remaining (${pend.length}/${reqs.length}):`,
|
|
486
|
+
...visible.map((r) => ` - ${truncate(r.title, 120)}`),
|
|
487
|
+
...(overflow > 0 ? [` ... y ${overflow} más`] : []),
|
|
488
|
+
);
|
|
489
|
+
updatedState.contractDisplayed = true;
|
|
490
|
+
persistTaskState(taskId, updatedState, wamRoot);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
try { initMemory(wamRoot); } catch {}
|
|
495
|
+
try { updateProjectMemo(analysis, wamRoot); } catch {}
|
|
496
|
+
try {
|
|
497
|
+
const pack = assembleContext({
|
|
498
|
+
prompt: promptText,
|
|
499
|
+
taskId,
|
|
500
|
+
classification: analysis.intent?.classification,
|
|
501
|
+
mode: analysis.strategy,
|
|
502
|
+
projectPath: wamRoot,
|
|
503
|
+
budget: cfg.contextBudget || 4000,
|
|
504
|
+
taskState: updatedState,
|
|
505
|
+
skillRegistry: waitAMinute.loadBundledRegistry(),
|
|
506
|
+
selectedSkills: analysis.skills?.selected || [],
|
|
507
|
+
});
|
|
508
|
+
if (pack.lines.length) {
|
|
509
|
+
inject.push(pack.lines.join("\n") + `\n[wam pack ${pack.budget_used}/${pack.budget} tok ${pack.levels.N0 ? "N0" : ""}${pack.levels.N1 ? "+N1" : ""}${pack.levels.N2 ? "+N2" : ""}${pack.levels.N3 ? "+N3" : ""}]`);
|
|
510
|
+
}
|
|
511
|
+
try { createSnapshot(taskId, updatedState, wamRoot); } catch {}
|
|
512
|
+
} catch {}
|
|
513
|
+
|
|
514
|
+
if (gate.blocked) {
|
|
515
|
+
const maxGateReqs = 5;
|
|
516
|
+
const pendingItems = gate.pending || [];
|
|
517
|
+
const visiblePending = pendingItems.slice(0, maxGateReqs);
|
|
518
|
+
const overflowPending = pendingItems.length - maxGateReqs;
|
|
519
|
+
const pendingList = visiblePending.length > 0
|
|
520
|
+
? "\n Requisitos pendientes:\n " + visiblePending.map((p) => `- ${truncate(p, 150)}`).join("\n ") + (overflowPending > 0 ? `\n ...(+${overflowPending} más)` : "") + "\n"
|
|
521
|
+
: "";
|
|
522
|
+
const gateHold = `⛔ [wait-a-minute] COMPLETION GATE: faltan ${gate.pending.length} requisito(s). No declare DONE.${pendingList} Continuar con: ${updatedState.nextAction}`;
|
|
523
|
+
emitTextPart(output, gateHold, { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
|
|
524
|
+
if (input?.parts && input.parts.length > 0) {
|
|
525
|
+
const tp = input.parts.find((p) => p.type === "text" && typeof p.text === "string");
|
|
526
|
+
if (tp) tp.text = gateHold;
|
|
527
|
+
}
|
|
528
|
+
} else if (gate.allDone) {
|
|
529
|
+
updateTaskMemory(taskId, {
|
|
530
|
+
summary: [
|
|
531
|
+
"# Task Summary", "", "## Objective",
|
|
532
|
+
`- ${taskId} (${analysis.intent?.classification || "task"})`, "", "## Completed",
|
|
533
|
+
...(updatedState.contract?.requirements || []).map((r) => `- ${r}`), "", "## Verification",
|
|
534
|
+
...(updatedState.requirements || []).map((r) => `- ${r.id}: ${r.status}`), "", "## Status", "COMPLETED",
|
|
535
|
+
].join("\n"),
|
|
536
|
+
}, wamRoot);
|
|
537
|
+
addRecentChange({
|
|
538
|
+
date: new Date().toISOString().slice(0, 10),
|
|
539
|
+
scope: taskId,
|
|
540
|
+
changes: updatedState.contract?.requirements || [],
|
|
541
|
+
verification: `requisitos completos: ${(updatedState.requirements || []).length}`,
|
|
542
|
+
}, wamRoot);
|
|
543
|
+
try {
|
|
544
|
+
writeCavemanSummary(taskId, updatedState, wamRoot, [
|
|
545
|
+
"date:", new Date().toISOString().slice(0, 10),
|
|
546
|
+
"scope:", taskId,
|
|
547
|
+
"status:", "COMPLETED",
|
|
548
|
+
"completed:",
|
|
549
|
+
...(updatedState.contract?.requirements || []).map((r) => `- ${r}`),
|
|
550
|
+
"verification:",
|
|
551
|
+
...(updatedState.requirements || []).map((r) => `- ${r.id}: ${r.status}`),
|
|
552
|
+
].join("\n"));
|
|
553
|
+
} catch {}
|
|
554
|
+
try {
|
|
555
|
+
closeSession({
|
|
556
|
+
sessionId: getSessionId(wamRoot),
|
|
557
|
+
taskId,
|
|
558
|
+
summary: (updatedState.contract?.requirements || []).join("; "),
|
|
559
|
+
candidates: (updatedState.requirements || []).map((r) => ({ id: r.id, title: r.title, evidence: r.evidence || [] })),
|
|
560
|
+
}, wamRoot);
|
|
561
|
+
} catch {}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (updatedState.contract?.status === "APPROVED") {
|
|
565
|
+
inject.push(...delegationLines(updatedState));
|
|
566
|
+
}
|
|
567
|
+
if (inject.length > 0) {
|
|
568
|
+
emitTextPart(output, inject.join("\n") + "\n", { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (updatedState.contract?.status !== "APPROVED" && updatedState.phase !== "DONE") {
|
|
572
|
+
waitAMinute.presentValidation({
|
|
573
|
+
analysis: {
|
|
574
|
+
...analysis,
|
|
575
|
+
contractStatus: updatedState.contract.status,
|
|
576
|
+
phase: updatedState.phase,
|
|
577
|
+
completionContract: updatedState.contract,
|
|
578
|
+
},
|
|
579
|
+
ctx: output,
|
|
580
|
+
meta: { sessionID: input.sessionID, messageID: output.message?.id || input.messageID },
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
input.waitAnalysis = analysis;
|
|
585
|
+
} catch (err) {
|
|
586
|
+
console.error("[wait-a-minute] Pre-flight analysis failed:", err);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Extracts the prompt text from a chat.message input/output pair.
|
|
592
|
+
* Extracted as a utility so it can be tested independently.
|
|
593
|
+
*
|
|
594
|
+
* @param {Object} input - chat.message input
|
|
595
|
+
* @param {Object} output - chat.message output
|
|
596
|
+
* @returns {string}
|
|
597
|
+
*/
|
|
598
|
+
export function extractPrompt(input, output) {
|
|
599
|
+
// Read ONLY from the user's own message. Never from `output.parts`:
|
|
600
|
+
// WAM writes synthetic injections there, so reading them back is the
|
|
601
|
+
// self-consumption bug that caused the re-entrancy loop.
|
|
602
|
+
const srcParts = input?.message?.parts || input?.parts || [];
|
|
603
|
+
if (srcParts.length > 0) {
|
|
604
|
+
const textPart = srcParts.find(
|
|
605
|
+
(p) => p.type === "text" && typeof p.text === "string" && !isWamSynthetic(p),
|
|
606
|
+
);
|
|
607
|
+
return textPart?.text || "";
|
|
608
|
+
}
|
|
609
|
+
if (input?.text && typeof input.text === "string") return input.text;
|
|
610
|
+
return "";
|
|
611
|
+
}
|