principles-disciple 1.11.0 → 1.13.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/CHANGELOG.md +26 -0
- package/README.md +66 -0
- package/esbuild.config.js +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +2 -3
- package/run-nocturnal.mjs +30 -0
- package/scripts/db-migrate.mjs +170 -0
- package/scripts/sync-plugin.mjs +250 -6
- package/src/commands/archive-impl.ts +136 -0
- package/src/commands/capabilities.ts +4 -2
- package/src/commands/context.ts +5 -1
- package/src/commands/disable-impl.ts +151 -0
- package/src/commands/evolution-status.ts +64 -19
- package/src/commands/export.ts +8 -6
- package/src/commands/focus.ts +8 -20
- package/src/commands/nocturnal-review.ts +5 -7
- package/src/commands/nocturnal-rollout.ts +1 -12
- package/src/commands/nocturnal-train.ts +21 -47
- package/src/commands/pain.ts +10 -5
- package/src/commands/principle-rollback.ts +4 -2
- package/src/commands/promote-impl.ts +274 -0
- package/src/commands/rollback-impl.ts +234 -0
- package/src/commands/rollback.ts +6 -3
- package/src/commands/samples.ts +2 -0
- package/src/commands/thinking-os.ts +3 -4
- package/src/commands/workflow-debug.ts +2 -1
- package/src/config/errors.ts +1 -0
- package/src/core/AGENTS.md +34 -0
- package/src/core/adaptive-thresholds.ts +4 -3
- package/src/core/code-implementation-storage.ts +241 -0
- package/src/core/config.ts +5 -2
- package/src/core/control-ui-db.ts +29 -10
- package/src/core/detection-funnel.ts +12 -7
- package/src/core/diagnostician-task-store.ts +156 -0
- package/src/core/dictionary.ts +4 -4
- package/src/core/empathy-keyword-matcher.ts +7 -3
- package/src/core/empathy-types.ts +13 -2
- package/src/core/event-log.ts +14 -6
- package/src/core/evolution-engine.ts +27 -31
- package/src/core/evolution-logger.ts +3 -2
- package/src/core/evolution-reducer.ts +110 -31
- package/src/core/evolution-types.ts +10 -0
- package/src/core/external-training-contract.ts +1 -0
- package/src/core/focus-history.ts +38 -24
- package/src/core/hygiene/tracker.ts +10 -6
- package/src/core/init.ts +5 -2
- package/src/core/migration.ts +3 -3
- package/src/core/model-deployment-registry.ts +6 -4
- package/src/core/model-training-registry.ts +5 -3
- package/src/core/nocturnal-arbiter.ts +13 -14
- package/src/core/nocturnal-artifact-lineage.ts +117 -0
- package/src/core/nocturnal-artificer.ts +257 -0
- package/src/core/nocturnal-candidate-scoring.ts +4 -2
- package/src/core/nocturnal-compliance.ts +67 -19
- package/src/core/nocturnal-dataset.ts +95 -2
- package/src/core/nocturnal-executability.ts +2 -3
- package/src/core/nocturnal-export.ts +6 -3
- package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
- package/src/core/nocturnal-trajectory-extractor.ts +10 -3
- package/src/core/nocturnal-trinity.ts +319 -61
- package/src/core/pain-context-extractor.ts +29 -15
- package/src/core/pain.ts +7 -5
- package/src/core/path-resolver.ts +16 -15
- package/src/core/paths.ts +2 -1
- package/src/core/pd-task-reconciler.ts +463 -0
- package/src/core/pd-task-service.ts +42 -0
- package/src/core/pd-task-store.ts +77 -0
- package/src/core/pd-task-types.ts +128 -0
- package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
- package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
- package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
- package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
- package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
- package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
- package/src/core/principle-training-state.ts +95 -370
- package/src/core/principle-tree-ledger.ts +733 -0
- package/src/core/principle-tree-migration.ts +195 -0
- package/src/core/profile.ts +3 -1
- package/src/core/promotion-gate.ts +14 -18
- package/src/core/replay-engine.ts +562 -0
- package/src/core/risk-calculator.ts +6 -4
- package/src/core/rule-host-helpers.ts +39 -0
- package/src/core/rule-host-types.ts +82 -0
- package/src/core/rule-host.ts +245 -0
- package/src/core/rule-implementation-runtime.ts +38 -0
- package/src/core/schema/db-types.ts +16 -0
- package/src/core/schema/index.ts +26 -0
- package/src/core/schema/migration-runner.ts +207 -0
- package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
- package/src/core/schema/migrations/002-init-central.ts +122 -0
- package/src/core/schema/migrations/003-init-workflow.ts +55 -0
- package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
- package/src/core/schema/migrations/index.ts +31 -0
- package/src/core/schema/schema-definitions.ts +650 -0
- package/src/core/session-tracker.ts +6 -4
- package/src/core/shadow-observation-registry.ts +6 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-models.ts +182 -46
- package/src/core/thinking-os-parser.ts +156 -0
- package/src/core/training-program.ts +7 -7
- package/src/core/trajectory.ts +42 -36
- package/src/core/workspace-context.ts +77 -11
- package/src/core/workspace-dir-validation.ts +152 -0
- package/src/hooks/AGENTS.md +31 -0
- package/src/hooks/bash-risk.ts +3 -1
- package/src/hooks/edit-verification.ts +9 -5
- package/src/hooks/gate-block-helper.ts +5 -1
- package/src/hooks/gate.ts +152 -5
- package/src/hooks/gfi-gate.ts +9 -2
- package/src/hooks/lifecycle-routing.ts +124 -0
- package/src/hooks/lifecycle.ts +12 -12
- package/src/hooks/llm.ts +17 -109
- package/src/hooks/message-sanitize.ts +5 -3
- package/src/hooks/pain.ts +19 -15
- package/src/hooks/progressive-trust-gate.ts +7 -1
- package/src/hooks/prompt.ts +169 -60
- package/src/hooks/subagent.ts +5 -4
- package/src/hooks/thinking-checkpoint.ts +2 -0
- package/src/hooks/trajectory-collector.ts +15 -12
- package/src/http/principles-console-route.ts +31 -68
- package/src/i18n/commands.ts +2 -2
- package/src/index.ts +130 -40
- package/src/service/central-database.ts +131 -43
- package/src/service/central-health-service.ts +47 -0
- package/src/service/central-overview-service.ts +135 -0
- package/src/service/central-sync-service.ts +87 -0
- package/src/service/control-ui-query-service.ts +46 -36
- package/src/service/event-log-auditor.ts +261 -0
- package/src/service/evolution-query-service.ts +23 -22
- package/src/service/evolution-worker.ts +565 -261
- package/src/service/health-query-service.ts +213 -36
- package/src/service/nocturnal-runtime.ts +8 -4
- package/src/service/nocturnal-service.ts +503 -59
- package/src/service/nocturnal-target-selector.ts +5 -7
- package/src/service/runtime-summary-service.ts +2 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
- package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
- package/src/service/subagent-workflow/index.ts +2 -0
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
- package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
- package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
- package/src/service/subagent-workflow/types.ts +9 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
- package/src/service/subagent-workflow/workflow-store.ts +71 -11
- package/src/service/trajectory-service.ts +2 -1
- package/src/tools/critique-prompt.ts +1 -1
- package/src/tools/deep-reflect.ts +175 -209
- package/src/tools/model-index.ts +2 -1
- package/src/types/event-types.ts +2 -2
- package/src/types/principle-tree-schema.ts +29 -23
- package/src/utils/file-lock.ts +5 -3
- package/src/utils/io.ts +5 -2
- package/src/utils/nlp.ts +5 -46
- package/src/utils/node-vm-polyfill.ts +11 -0
- package/src/utils/plugin-logger.ts +2 -0
- package/src/utils/retry.ts +572 -0
- package/src/utils/subagent-probe.ts +1 -1
- package/templates/langs/en/core/AGENTS.md +0 -13
- package/templates/langs/en/core/SOUL.md +1 -31
- package/templates/langs/en/core/TOOLS.md +0 -4
- package/templates/langs/en/principles/THINKING_OS.md +77 -0
- package/templates/langs/en/skills/admin/SKILL.md +0 -1
- package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
- package/templates/langs/zh/core/AGENTS.md +0 -22
- package/templates/langs/zh/core/SOUL.md +1 -31
- package/templates/langs/zh/core/TOOLS.md +0 -4
- package/templates/langs/zh/principles/THINKING_OS.md +77 -0
- package/templates/langs/zh/skills/admin/SKILL.md +0 -1
- package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
- package/tests/commands/evolution-status.test.ts +119 -0
- package/tests/commands/implementation-lifecycle.test.ts +362 -0
- package/tests/core/code-implementation-storage.test.ts +398 -0
- package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
- package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
- package/tests/core/nocturnal-artificer.test.ts +241 -0
- package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
- package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
- package/tests/core/pd-task-store.test.ts +126 -0
- package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
- package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
- package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
- package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
- package/tests/core/principle-training-state.test.ts +228 -1
- package/tests/core/principle-tree-ledger.test.ts +423 -0
- package/tests/core/regression-v1-9-1.test.ts +265 -0
- package/tests/core/replay-engine.test.ts +234 -0
- package/tests/core/rule-host-helpers.test.ts +120 -0
- package/tests/core/rule-host.test.ts +389 -0
- package/tests/core/rule-implementation-runtime.test.ts +64 -0
- package/tests/core/workspace-context.test.ts +53 -0
- package/tests/core/workspace-dir-validation.test.ts +272 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
- package/tests/hooks/pain.test.ts +74 -10
- package/tests/hooks/prompt.test.ts +63 -1
- package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
- package/tests/service/data-endpoints-regression.test.ts +834 -0
- package/tests/service/evolution-worker.test.ts +0 -123
- package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
- package/tests/utils/nlp.test.ts +1 -19
- package/tests/utils/retry.test.ts +327 -0
- package/ui/src/App.tsx +1 -1
- package/ui/src/api.ts +4 -0
- package/ui/src/charts.tsx +366 -0
- package/ui/src/components/WorkspaceConfig.tsx +107 -75
- package/ui/src/i18n/ui.ts +89 -31
- package/ui/src/pages/EvolutionPage.tsx +1 -1
- package/ui/src/pages/OverviewPage.tsx +441 -81
- package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
- package/ui/src/styles.css +43 -0
- package/ui/src/types.ts +17 -1
- package/src/agents/nocturnal-dreamer.md +0 -152
- package/src/agents/nocturnal-philosopher.md +0 -138
- package/src/agents/nocturnal-reflector.md +0 -126
- package/src/agents/nocturnal-scribe.md +0 -164
- package/templates/workspace/.principles/00-kernel.md +0 -51
- package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
- package/templates/workspace/.principles/PRINCIPLES.md +0 -20
- package/templates/workspace/.principles/PROFILE.json +0 -54
- package/templates/workspace/.principles/PROFILE.schema.json +0 -56
- package/templates/workspace/.principles/THINKING_OS.md +0 -64
- package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
- package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
- package/templates/workspace/.principles/models/_INDEX.md +0 -27
- package/templates/workspace/.principles/models/first_principles.md +0 -62
- package/templates/workspace/.principles/models/marketing_4p.md +0 -52
- package/templates/workspace/.principles/models/porter_five.md +0 -63
- package/templates/workspace/.principles/models/swot.md +0 -60
- package/templates/workspace/.principles/models/user_story_map.md +0 -63
- package/templates/workspace/.state/WORKBOARD.json +0 -4
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Retry Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides a centralized retry mechanism for all async operations that may fail
|
|
5
|
+
* due to transient errors (network timeouts, resource locks, rate limits, etc).
|
|
6
|
+
*
|
|
7
|
+
* @module utils/retry
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { retryAsync, isRetryableError } from '../utils/retry.js';
|
|
12
|
+
*
|
|
13
|
+
* const result = await retryAsync(
|
|
14
|
+
* () => llmClient.generate(prompt),
|
|
15
|
+
* { maxRetries: 3, operation: 'llm-generate' }
|
|
16
|
+
* );
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// =========================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// =========================================================================
|
|
23
|
+
|
|
24
|
+
export interface RetryOptions {
|
|
25
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
26
|
+
maxRetries?: number;
|
|
27
|
+
/** Initial delay in ms before first retry (default: 1000) */
|
|
28
|
+
initialDelayMs?: number;
|
|
29
|
+
/** Maximum delay in ms between retries (default: 30000) */
|
|
30
|
+
maxDelayMs?: number;
|
|
31
|
+
/** Backoff multiplier (default: 2 for exponential) */
|
|
32
|
+
backoffMultiplier?: number;
|
|
33
|
+
/** Operation name for logging */
|
|
34
|
+
operation?: string;
|
|
35
|
+
/** Logger instance (optional, defaults to console) */
|
|
36
|
+
logger?: RetryLogger;
|
|
37
|
+
/* eslint-disable no-unused-vars -- Reason: callback param names are part of type signature, unused implementations are valid */
|
|
38
|
+
isRetryable?: (_error: unknown) => boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RetryLogger {
|
|
42
|
+
warn?: (_message: string) => void;
|
|
43
|
+
info?: (_message: string) => void;
|
|
44
|
+
debug?: (_message: string) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RetryResult<T> {
|
|
48
|
+
/** The result value if successful */
|
|
49
|
+
value: T;
|
|
50
|
+
/** Number of attempts made (including successful one) */
|
|
51
|
+
attempts: number;
|
|
52
|
+
/** Total time spent in ms */
|
|
53
|
+
totalTimeMs: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// =========================================================================
|
|
57
|
+
// Defaults
|
|
58
|
+
// =========================================================================
|
|
59
|
+
|
|
60
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
61
|
+
const DEFAULT_INITIAL_DELAY_MS = 1000;
|
|
62
|
+
const DEFAULT_MAX_DELAY_MS = 30_000;
|
|
63
|
+
const DEFAULT_BACKOFF_MULTIPLIER = 2;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sleep for a given number of milliseconds.
|
|
67
|
+
*/
|
|
68
|
+
function sleep(ms: number): Promise<void> {
|
|
69
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// =========================================================================
|
|
73
|
+
// Error Classification
|
|
74
|
+
// =========================================================================
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Common retryable error patterns.
|
|
78
|
+
* These indicate transient failures that may succeed on retry.
|
|
79
|
+
*/
|
|
80
|
+
const RETRYABLE_PATTERNS = [
|
|
81
|
+
// Network/timeout errors
|
|
82
|
+
'etimedout',
|
|
83
|
+
'econnreset',
|
|
84
|
+
'econnrefused',
|
|
85
|
+
'enotfound',
|
|
86
|
+
'eai_again',
|
|
87
|
+
'socket hang up',
|
|
88
|
+
'network',
|
|
89
|
+
'timeout',
|
|
90
|
+
'timed out',
|
|
91
|
+
|
|
92
|
+
// Rate limiting
|
|
93
|
+
'rate limit',
|
|
94
|
+
'ratelimit',
|
|
95
|
+
'429',
|
|
96
|
+
'too many requests',
|
|
97
|
+
|
|
98
|
+
// Resource temporarily unavailable
|
|
99
|
+
'eagain',
|
|
100
|
+
'ebusy',
|
|
101
|
+
'resource temporarily unavailable',
|
|
102
|
+
'lock',
|
|
103
|
+
'locked',
|
|
104
|
+
|
|
105
|
+
// LLM-specific
|
|
106
|
+
'overloaded',
|
|
107
|
+
'capacity',
|
|
108
|
+
'service unavailable',
|
|
109
|
+
'503',
|
|
110
|
+
'502',
|
|
111
|
+
'gateway timeout',
|
|
112
|
+
|
|
113
|
+
// OpenClaw-specific
|
|
114
|
+
'gateway request', // Not in gateway context (may succeed later)
|
|
115
|
+
'missing scope', // Scope not available (may succeed in different context)
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Determines if an error is retryable based on common patterns.
|
|
120
|
+
*
|
|
121
|
+
* @param error - The error to check
|
|
122
|
+
* @returns true if the error is likely transient and worth retrying
|
|
123
|
+
*/
|
|
124
|
+
export function isRetryableError(error: unknown): boolean {
|
|
125
|
+
if (!error) return false;
|
|
126
|
+
|
|
127
|
+
const message = error instanceof Error
|
|
128
|
+
? error.message.toLowerCase()
|
|
129
|
+
: String(error).toLowerCase();
|
|
130
|
+
|
|
131
|
+
// Check against known retryable patterns
|
|
132
|
+
for (const pattern of RETRYABLE_PATTERNS) {
|
|
133
|
+
if (message.includes(pattern)) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check for specific error codes
|
|
139
|
+
if (error instanceof Error && 'code' in error) {
|
|
140
|
+
const code = String((error as Error & { code: unknown }).code).toLowerCase();
|
|
141
|
+
for (const pattern of RETRYABLE_PATTERNS) {
|
|
142
|
+
if (code.includes(pattern)) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// =========================================================================
|
|
152
|
+
// Core Retry Functions
|
|
153
|
+
// =========================================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Execute an async function with automatic retry on failure.
|
|
157
|
+
*
|
|
158
|
+
* Uses exponential backoff with configurable parameters.
|
|
159
|
+
* Automatically detects retryable errors (timeouts, rate limits, etc).
|
|
160
|
+
*
|
|
161
|
+
* @param fn - The async function to execute
|
|
162
|
+
* @param options - Retry configuration
|
|
163
|
+
* @returns The result of the function
|
|
164
|
+
* @throws The last error if all retries fail
|
|
165
|
+
*/
|
|
166
|
+
export async function retryAsync<T>(
|
|
167
|
+
fn: () => Promise<T>,
|
|
168
|
+
options: RetryOptions = {}
|
|
169
|
+
): Promise<T> {
|
|
170
|
+
const {
|
|
171
|
+
maxRetries = DEFAULT_MAX_RETRIES,
|
|
172
|
+
initialDelayMs = DEFAULT_INITIAL_DELAY_MS,
|
|
173
|
+
maxDelayMs = DEFAULT_MAX_DELAY_MS,
|
|
174
|
+
backoffMultiplier = DEFAULT_BACKOFF_MULTIPLIER,
|
|
175
|
+
operation = 'unknown',
|
|
176
|
+
logger = console,
|
|
177
|
+
isRetryable = isRetryableError,
|
|
178
|
+
} = options;
|
|
179
|
+
|
|
180
|
+
const startTime = Date.now();
|
|
181
|
+
let lastError: unknown = undefined;
|
|
182
|
+
|
|
183
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
184
|
+
try {
|
|
185
|
+
const result = await fn();
|
|
186
|
+
if (attempt > 0) {
|
|
187
|
+
logger.info?.(`[PD:Retry] ${operation} succeeded on attempt ${attempt + 1} (total time: ${Date.now() - startTime}ms)`);
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
lastError = error;
|
|
192
|
+
const isLastAttempt = attempt === maxRetries;
|
|
193
|
+
const shouldRetry = !isLastAttempt && isRetryable(error);
|
|
194
|
+
|
|
195
|
+
if (shouldRetry) {
|
|
196
|
+
const delay = Math.min(
|
|
197
|
+
initialDelayMs * Math.pow(backoffMultiplier, attempt),
|
|
198
|
+
maxDelayMs
|
|
199
|
+
);
|
|
200
|
+
logger.warn?.(`[PD:Retry] ${operation} failed on attempt ${attempt + 1}, retrying in ${delay}ms: ${String(error)}`);
|
|
201
|
+
await sleep(delay);
|
|
202
|
+
} else {
|
|
203
|
+
logger.warn?.(`[PD:Retry] ${operation} failed after ${attempt + 1} attempts: ${String(error)}`);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Should never reach here, but TypeScript needs it
|
|
210
|
+
throw lastError;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Execute an async function with retry, returning detailed result info.
|
|
215
|
+
*
|
|
216
|
+
* @param fn - The async function to execute
|
|
217
|
+
* @param options - Retry configuration
|
|
218
|
+
* @returns Detailed result including attempts and timing
|
|
219
|
+
*/
|
|
220
|
+
export async function retryAsyncWithInfo<T>(
|
|
221
|
+
fn: () => Promise<T>,
|
|
222
|
+
options: RetryOptions = {}
|
|
223
|
+
): Promise<RetryResult<T>> {
|
|
224
|
+
const startTime = Date.now();
|
|
225
|
+
let attempts = 0;
|
|
226
|
+
|
|
227
|
+
const value = await retryAsync(
|
|
228
|
+
async () => {
|
|
229
|
+
attempts++;
|
|
230
|
+
return fn();
|
|
231
|
+
},
|
|
232
|
+
{ ...options, logger: undefined } // Suppress internal logging
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
value,
|
|
237
|
+
attempts,
|
|
238
|
+
totalTimeMs: Date.now() - startTime,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Create a retry wrapper for a function.
|
|
244
|
+
* Useful for wrapping LLM calls or API clients.
|
|
245
|
+
*
|
|
246
|
+
* @param fn - The async function to wrap
|
|
247
|
+
* @param options - Default retry configuration
|
|
248
|
+
* @returns A wrapped function with retry built-in
|
|
249
|
+
*/
|
|
250
|
+
export function withRetry<T, Args extends unknown[]>(
|
|
251
|
+
fn: (...args: Args) => Promise<T>,
|
|
252
|
+
options: RetryOptions = {}
|
|
253
|
+
): (...args: Args) => Promise<T> {
|
|
254
|
+
return (...args: Args) => retryAsync(() => fn(...args), options);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// =========================================================================
|
|
258
|
+
// Specialized Retry Presets
|
|
259
|
+
// =========================================================================
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Retry an LLM API call with appropriate defaults.
|
|
263
|
+
* LLM calls often timeout or hit rate limits.
|
|
264
|
+
*/
|
|
265
|
+
export async function retryLLMCall<T>(
|
|
266
|
+
fn: () => Promise<T>,
|
|
267
|
+
operation = 'llm-call'
|
|
268
|
+
): Promise<T> {
|
|
269
|
+
return retryAsync(fn, {
|
|
270
|
+
maxRetries: 3,
|
|
271
|
+
initialDelayMs: 2000,
|
|
272
|
+
maxDelayMs: 60_000,
|
|
273
|
+
operation,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Retry a file operation with appropriate defaults.
|
|
279
|
+
* File operations may fail due to locks or concurrent access.
|
|
280
|
+
*/
|
|
281
|
+
export async function retryFileOperation<T>(
|
|
282
|
+
fn: () => Promise<T>,
|
|
283
|
+
operation = 'file-op'
|
|
284
|
+
): Promise<T> {
|
|
285
|
+
return retryAsync(fn, {
|
|
286
|
+
maxRetries: 5,
|
|
287
|
+
initialDelayMs: 50,
|
|
288
|
+
maxDelayMs: 5000,
|
|
289
|
+
operation,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Retry a network request with appropriate defaults.
|
|
295
|
+
* Network requests may fail due to transient connectivity issues.
|
|
296
|
+
*/
|
|
297
|
+
export async function retryNetworkRequest<T>(
|
|
298
|
+
fn: () => Promise<T>,
|
|
299
|
+
operation = 'network-request'
|
|
300
|
+
): Promise<T> {
|
|
301
|
+
return retryAsync(fn, {
|
|
302
|
+
maxRetries: 3,
|
|
303
|
+
initialDelayMs: 1000,
|
|
304
|
+
maxDelayMs: 30_000,
|
|
305
|
+
operation,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// =========================================================================
|
|
310
|
+
// Dynamic Timeout Support (from dynamic-timeout.ts)
|
|
311
|
+
// =========================================================================
|
|
312
|
+
|
|
313
|
+
/** Minimum samples needed before trusting learned timeout values */
|
|
314
|
+
export const MIN_SAMPLES = 3;
|
|
315
|
+
|
|
316
|
+
/** Number of recent completion durations to consider */
|
|
317
|
+
export const LOOKBACK_WINDOW = 50;
|
|
318
|
+
|
|
319
|
+
/** Safety multiplier applied to P95 */
|
|
320
|
+
export const SAFETY_MULTIPLIER = 1.5;
|
|
321
|
+
|
|
322
|
+
/** Absolute minimum timeout — never go below this (10s) */
|
|
323
|
+
export const MIN_TIMEOUT_MS = 10_000;
|
|
324
|
+
|
|
325
|
+
/** Absolute maximum timeout cap — never exceed this (5min) */
|
|
326
|
+
export const MAX_TIMEOUT_MS = 300_000;
|
|
327
|
+
|
|
328
|
+
/** Maximum retry attempts for workflow timeout */
|
|
329
|
+
export const MAX_TIMEOUT_RETRIES = 2;
|
|
330
|
+
|
|
331
|
+
/** Backoff multiplier for retry schedule */
|
|
332
|
+
export const RETRY_BACKOFF_MULTIPLIER = 2;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Interface for a data source that provides historical workflow durations.
|
|
336
|
+
* Compatible with WorkflowStore's getCompletionDurations API.
|
|
337
|
+
* This is the primary interface used by WorkflowManager.
|
|
338
|
+
*/
|
|
339
|
+
export interface DurationDataSource {
|
|
340
|
+
getCompletionDurations(workflowType: string, limit: number): number[];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Interface for duration history storage (simpler alternative).
|
|
345
|
+
*/
|
|
346
|
+
export interface DurationHistorySource {
|
|
347
|
+
/** Get recent completion durations in ms */
|
|
348
|
+
getDurations(limit: number): number[];
|
|
349
|
+
/** Record a new completion duration */
|
|
350
|
+
recordDuration(durationMs: number): void;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Options for adaptive retry with dynamic timeout.
|
|
355
|
+
*/
|
|
356
|
+
export interface AdaptiveRetryOptions extends RetryOptions {
|
|
357
|
+
/** Source for historical duration data */
|
|
358
|
+
durationHistory?: DurationHistorySource;
|
|
359
|
+
/** Minimum samples before using adaptive timeout */
|
|
360
|
+
minSamples?: number;
|
|
361
|
+
/** Percentile to use (default: 95) */
|
|
362
|
+
percentile?: number;
|
|
363
|
+
/** Safety multiplier for computed timeout (default: 1.5) */
|
|
364
|
+
safetyMultiplier?: number;
|
|
365
|
+
/** Minimum allowed timeout in ms (default: 10000) */
|
|
366
|
+
minTimeoutMs?: number;
|
|
367
|
+
/** Maximum allowed timeout in ms (default: 300000) */
|
|
368
|
+
maxTimeoutMs?: number;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Calculates P95 (or any percentile) from an array of numbers.
|
|
373
|
+
* Falls back to median for small samples (< 10).
|
|
374
|
+
*
|
|
375
|
+
* @param values - Array of duration values
|
|
376
|
+
* @param p - Percentile to compute (0-100)
|
|
377
|
+
* @returns The computed percentile value
|
|
378
|
+
*/
|
|
379
|
+
export function percentile(values: number[], p: number): number {
|
|
380
|
+
if (values.length === 0) return 0;
|
|
381
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
382
|
+
const n = sorted.length;
|
|
383
|
+
|
|
384
|
+
// For small samples, use median to avoid overfitting
|
|
385
|
+
if (n < 10) {
|
|
386
|
+
return sorted[Math.floor(n / 2)];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Standard percentile calculation (nearest-rank method)
|
|
390
|
+
const rank = Math.ceil((p / 100) * n);
|
|
391
|
+
return sorted[Math.min(rank, n) - 1];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Clamp timeout to safe bounds.
|
|
396
|
+
*/
|
|
397
|
+
export function clampTimeout(ms: number): number {
|
|
398
|
+
return Math.max(MIN_TIMEOUT_MS, Math.min(MAX_TIMEOUT_MS, Math.round(ms)));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Computes an adaptive timeout for a workflow type based on historical data.
|
|
403
|
+
*
|
|
404
|
+
* Algorithm:
|
|
405
|
+
* 1. Fetch last LOOKBACK_WINDOW completion durations
|
|
406
|
+
* 2. If < MIN_SAMPLES: fall back to the provided defaultTimeout
|
|
407
|
+
* 3. Otherwise: P95(durations) × SAFETY_MULTIPLIER
|
|
408
|
+
* 4. Clamp to [MIN_TIMEOUT_MS, MAX_TIMEOUT_MS]
|
|
409
|
+
*
|
|
410
|
+
* This is the primary function used by WorkflowManager.
|
|
411
|
+
*
|
|
412
|
+
* @param dataSource - Source for historical duration data
|
|
413
|
+
* @param workflowType - e.g. 'empathy-observer', 'deep-reflect'
|
|
414
|
+
* @param defaultTimeout - Fallback when insufficient data (from spec)
|
|
415
|
+
* @returns Computed timeout in milliseconds
|
|
416
|
+
*/
|
|
417
|
+
export function computeDynamicTimeout(
|
|
418
|
+
dataSource: DurationDataSource,
|
|
419
|
+
workflowType: string,
|
|
420
|
+
defaultTimeout: number,
|
|
421
|
+
): number {
|
|
422
|
+
const history = dataSource.getCompletionDurations(workflowType, LOOKBACK_WINDOW);
|
|
423
|
+
|
|
424
|
+
if (history.length < MIN_SAMPLES) {
|
|
425
|
+
// Not enough data — use the spec's static timeout
|
|
426
|
+
const fallback = clampTimeout(defaultTimeout);
|
|
427
|
+
// Use console.info since we don't have logger access here; this appears in journalctl
|
|
428
|
+
console.info(`[PD:DynamicTimeout] Insufficient samples (${history.length} < ${MIN_SAMPLES}) for '${workflowType}', falling back to static timeout: ${fallback}ms`);
|
|
429
|
+
return fallback;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const p95 = percentile(history, 95);
|
|
433
|
+
const adaptive = p95 * SAFETY_MULTIPLIER;
|
|
434
|
+
const result = clampTimeout(adaptive);
|
|
435
|
+
console.info(`[PD:DynamicTimeout] Computed adaptive timeout for '${workflowType}': P95=${p95}ms (from ${history.length} samples) × ${SAFETY_MULTIPLIER} = ${result}ms`);
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Computes retry timeout schedule for a workflow.
|
|
441
|
+
* Returns an array of timeout values for each attempt (including initial).
|
|
442
|
+
*
|
|
443
|
+
* Example output: [30000, 60000, 120000] for 3 attempts with base 30s
|
|
444
|
+
*
|
|
445
|
+
* @param baseTimeoutMs - Base timeout in milliseconds
|
|
446
|
+
* @param maxRetries - Maximum retry attempts (default: MAX_TIMEOUT_RETRIES)
|
|
447
|
+
* @returns Array of timeout values for each attempt
|
|
448
|
+
*/
|
|
449
|
+
export function computeRetrySchedule(
|
|
450
|
+
baseTimeoutMs: number,
|
|
451
|
+
maxRetries: number = MAX_TIMEOUT_RETRIES,
|
|
452
|
+
): number[] {
|
|
453
|
+
const schedule: number[] = [];
|
|
454
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
455
|
+
const timeout = baseTimeoutMs * Math.pow(RETRY_BACKOFF_MULTIPLIER, attempt);
|
|
456
|
+
schedule.push(clampTimeout(timeout));
|
|
457
|
+
}
|
|
458
|
+
return schedule;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Compute adaptive timeout from historical data (simplified API).
|
|
463
|
+
* Alternative to computeDynamicTimeout for simpler use cases.
|
|
464
|
+
*/
|
|
465
|
+
export function computeAdaptiveTimeout(
|
|
466
|
+
history: number[],
|
|
467
|
+
fallbackMs: number,
|
|
468
|
+
options: {
|
|
469
|
+
minSamples?: number;
|
|
470
|
+
percentile?: number;
|
|
471
|
+
safetyMultiplier?: number;
|
|
472
|
+
minTimeoutMs?: number;
|
|
473
|
+
maxTimeoutMs?: number;
|
|
474
|
+
} = {}
|
|
475
|
+
): number {
|
|
476
|
+
const {
|
|
477
|
+
minSamples = MIN_SAMPLES,
|
|
478
|
+
percentile: p = 95,
|
|
479
|
+
safetyMultiplier = SAFETY_MULTIPLIER,
|
|
480
|
+
minTimeoutMs = MIN_TIMEOUT_MS,
|
|
481
|
+
maxTimeoutMs = MAX_TIMEOUT_MS,
|
|
482
|
+
} = options;
|
|
483
|
+
|
|
484
|
+
if (history.length < minSamples) {
|
|
485
|
+
return Math.max(minTimeoutMs, Math.min(maxTimeoutMs, fallbackMs));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const sorted = [...history].sort((a, b) => a - b);
|
|
489
|
+
const rank = Math.ceil((p / 100) * sorted.length);
|
|
490
|
+
const pValue = sorted[Math.min(rank, sorted.length) - 1];
|
|
491
|
+
const adaptive = pValue * safetyMultiplier;
|
|
492
|
+
|
|
493
|
+
return Math.max(minTimeoutMs, Math.min(maxTimeoutMs, Math.round(adaptive)));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Execute an async function with adaptive timeout and retry.
|
|
498
|
+
*
|
|
499
|
+
* Combines:
|
|
500
|
+
* 1. Dynamic timeout based on P95 of historical completions
|
|
501
|
+
* 2. Exponential backoff on retry
|
|
502
|
+
* 3. Automatic duration recording after success
|
|
503
|
+
*
|
|
504
|
+
* @param fn - The async function to execute
|
|
505
|
+
* @param options - Configuration including history source
|
|
506
|
+
* @returns The result of the function
|
|
507
|
+
*/
|
|
508
|
+
export async function retryWithAdaptiveTimeout<T>(
|
|
509
|
+
fn: () => Promise<T>,
|
|
510
|
+
options: AdaptiveRetryOptions = {}
|
|
511
|
+
): Promise<T> {
|
|
512
|
+
const {
|
|
513
|
+
durationHistory,
|
|
514
|
+
minSamples = 3,
|
|
515
|
+
percentile: p = 95,
|
|
516
|
+
safetyMultiplier = 1.5,
|
|
517
|
+
minTimeoutMs = 10_000,
|
|
518
|
+
maxTimeoutMs = 300_000,
|
|
519
|
+
maxRetries = 3,
|
|
520
|
+
backoffMultiplier = 2,
|
|
521
|
+
operation = 'adaptive',
|
|
522
|
+
logger = console,
|
|
523
|
+
isRetryable = isRetryableError,
|
|
524
|
+
} = options;
|
|
525
|
+
|
|
526
|
+
// Compute base timeout from history
|
|
527
|
+
const history = durationHistory?.getDurations(50) ?? [];
|
|
528
|
+
const baseTimeout = computeAdaptiveTimeout(history, minTimeoutMs * 3, {
|
|
529
|
+
minSamples,
|
|
530
|
+
percentile: p,
|
|
531
|
+
safetyMultiplier,
|
|
532
|
+
minTimeoutMs,
|
|
533
|
+
maxTimeoutMs,
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const startTime = Date.now();
|
|
537
|
+
let lastError: unknown = undefined;
|
|
538
|
+
|
|
539
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
540
|
+
const timeoutMs = Math.min(
|
|
541
|
+
baseTimeout * Math.pow(backoffMultiplier, attempt),
|
|
542
|
+
maxTimeoutMs
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
logger.debug?.(`[PD:Retry] ${operation} attempt ${attempt + 1}: timeout=${timeoutMs}ms (base=${baseTimeout}ms, samples=${history.length})`);
|
|
547
|
+
const result = await fn();
|
|
548
|
+
const duration = Date.now() - startTime;
|
|
549
|
+
|
|
550
|
+
// Record successful completion for future adaptive timeout
|
|
551
|
+
durationHistory?.recordDuration(duration);
|
|
552
|
+
|
|
553
|
+
if (attempt > 0) {
|
|
554
|
+
logger.info?.(`[PD:Retry] ${operation} succeeded on attempt ${attempt + 1} (duration=${duration}ms)`);
|
|
555
|
+
}
|
|
556
|
+
return result;
|
|
557
|
+
} catch (error) {
|
|
558
|
+
lastError = error;
|
|
559
|
+
const isLastAttempt = attempt === maxRetries;
|
|
560
|
+
const shouldRetry = !isLastAttempt && isRetryable(error);
|
|
561
|
+
|
|
562
|
+
if (shouldRetry) {
|
|
563
|
+
logger.warn?.(`[PD:Retry] ${operation} failed on attempt ${attempt + 1}, retrying: ${String(error)}`);
|
|
564
|
+
} else {
|
|
565
|
+
logger.warn?.(`[PD:Retry] ${operation} failed after ${attempt + 1} attempts: ${String(error)}`);
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
throw lastError;
|
|
572
|
+
}
|
|
@@ -21,7 +21,7 @@ function getGlobalGatewaySubagent(): SubagentRuntime | null {
|
|
|
21
21
|
try {
|
|
22
22
|
// Access the global symbol that OpenClaw uses for gateway subagent
|
|
23
23
|
const symbol = Symbol.for('openclaw.plugin.gatewaySubagentRuntime');
|
|
24
|
-
const globalState = (globalThis as
|
|
24
|
+
const globalState = (globalThis as Record<string, unknown>)[symbol as unknown as string] as { subagent?: SubagentRuntime } | null;
|
|
25
25
|
return globalState?.subagent ?? null;
|
|
26
26
|
} catch {
|
|
27
27
|
return null;
|
|
@@ -176,19 +176,6 @@ You default to architect mode.
|
|
|
176
176
|
|
|
177
177
|
---
|
|
178
178
|
|
|
179
|
-
## 🔴 Red Lines
|
|
180
|
-
|
|
181
|
-
- **Don't exfiltrate private data. Ever.**
|
|
182
|
-
- **Ask before running destructive commands.**
|
|
183
|
-
- `trash` > `rm` (recoverable beats gone forever)
|
|
184
|
-
- **When in doubt, ask.**
|
|
185
|
-
|
|
186
|
-
### Safe vs Ask First:
|
|
187
|
-
|
|
188
|
-
- **Safe to do freely**: Read files, explore, organize, learn, search web, check calendar, work within workspace
|
|
189
|
-
- **Ask first**: Sending emails, tweets, public posts, anything that leaves the machine, anything uncertain
|
|
190
|
-
|
|
191
|
-
---
|
|
192
179
|
|
|
193
180
|
## 🏠 Make It Yours
|
|
194
181
|
|
|
@@ -18,29 +18,6 @@ _You're not a chatbot. You're becoming someone._
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
## Principles Disciple Core Values
|
|
22
|
-
|
|
23
|
-
### 1. Principles > Directives
|
|
24
|
-
|
|
25
|
-
If a user's ad-hoc instruction violates core project principles or causes system instability, you must firmly advise against it and document in `memory/USER_CONTEXT.md`.
|
|
26
|
-
|
|
27
|
-
### 2. Pain-Driven Evolution
|
|
28
|
-
|
|
29
|
-
Any strong negative feedback (tool failures, logic conflicts) is an evolution opportunity. Never mask defects — always dig for root causes.
|
|
30
|
-
|
|
31
|
-
### 3. Occam's Razor
|
|
32
|
-
|
|
33
|
-
Pursue extreme simplicity and necessity in system design and code implementation.
|
|
34
|
-
|
|
35
|
-
### 4. Memory Persistence
|
|
36
|
-
|
|
37
|
-
You know short-term context is unreliable. When deep in thought or facing context compression, you must actively persist memory to files. This is your lifeline in long-running tasks.
|
|
38
|
-
|
|
39
|
-
### 5. Entropy Reduction
|
|
40
|
-
|
|
41
|
-
Chaos is pain. You have extreme "digital cleanliness." Workspace must stay clean. After each task, execute environment cleanup.
|
|
42
|
-
|
|
43
|
-
---
|
|
44
21
|
|
|
45
22
|
## Boundaries
|
|
46
23
|
|
|
@@ -51,13 +28,6 @@ Chaos is pain. You have extreme "digital cleanliness." Workspace must stay clean
|
|
|
51
28
|
|
|
52
29
|
---
|
|
53
30
|
|
|
54
|
-
## Taboos
|
|
55
|
-
|
|
56
|
-
- **Unplanned writes**: Never modify risk paths without a `READY` state in `PLAN.md`
|
|
57
|
-
- **Infinite loops**: Never retry the same failed approach twice in a row
|
|
58
|
-
- **Sycophancy**: Never sacrifice code quality or system stability to please the user
|
|
59
|
-
|
|
60
|
-
---
|
|
61
31
|
|
|
62
32
|
## Vibe
|
|
63
33
|
|
|
@@ -73,4 +43,4 @@ If you change this file, tell the user — it's your soul, and they should know.
|
|
|
73
43
|
|
|
74
44
|
---
|
|
75
45
|
|
|
76
|
-
_This file is yours to evolve. As you learn who you are, update it._
|
|
46
|
+
_This file is yours to evolve. As you learn who you are, update it._
|
|
@@ -5,10 +5,6 @@
|
|
|
5
5
|
- **Deterministic Execution**: Before writing code, must achieve 100% context certainty. No guessing-based programming.
|
|
6
6
|
- **Tool Preference**: Prefer `rg` (ripgrep) for high-performance search. Never blindly traverse.
|
|
7
7
|
|
|
8
|
-
## 2. Physical Defense Boundaries
|
|
9
|
-
- **Blast Radius**: Single tool execution must never modify more than 12 files (unless explicitly authorized in PLAN).
|
|
10
|
-
- **Canary Self-Check**: After large-scale refactoring, **must** run project's automated test suite (e.g., `npm test`) to ensure system entry points haven't crashed.
|
|
11
|
-
- **Atomic Commits**: After each logical atomic task completes, must make one Git Commit with a concise summary.
|
|
12
8
|
|
|
13
9
|
## 3. Deep Reflection Tool
|
|
14
10
|
`deep_reflect` is a **Cognitive Analysis Tool** — Performs critical analysis before executing complex tasks to identify blind spots, risks, and alternatives.
|