discoclaw 0.1.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/.context/README.md +42 -0
- package/.context/architecture.md +58 -0
- package/.context/bot-setup.md +24 -0
- package/.context/dev.md +230 -0
- package/.context/discord.md +144 -0
- package/.context/memory.md +257 -0
- package/.context/ops.md +59 -0
- package/.context/pa-safety.md +47 -0
- package/.context/pa.md +118 -0
- package/.context/project.md +43 -0
- package/.context/runtime.md +253 -0
- package/.context/tasks.md +71 -0
- package/.context/tools.md +75 -0
- package/.env.example +88 -0
- package/.env.example.full +378 -0
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/beads/auto-tag.js +2 -0
- package/dist/beads/auto-tag.test.js +62 -0
- package/dist/beads/bd-cli.js +9 -0
- package/dist/beads/bd-cli.test.js +495 -0
- package/dist/beads/bead-hooks-cli.js +149 -0
- package/dist/beads/bead-sync-cli.js +5 -0
- package/dist/beads/bead-sync-cli.test.js +72 -0
- package/dist/beads/bead-sync-coordinator.js +4 -0
- package/dist/beads/bead-sync-coordinator.test.js +239 -0
- package/dist/beads/bead-sync-watcher.js +2 -0
- package/dist/beads/bead-sync-watcher.test.js +96 -0
- package/dist/beads/bead-sync.js +7 -0
- package/dist/beads/bead-sync.test.js +876 -0
- package/dist/beads/bead-thread-cache.js +8 -0
- package/dist/beads/bead-thread-cache.test.js +91 -0
- package/dist/beads/discord-sync.js +18 -0
- package/dist/beads/discord-sync.test.js +782 -0
- package/dist/beads/find-bead-by-thread.test.js +36 -0
- package/dist/beads/forum-guard.js +2 -0
- package/dist/beads/forum-guard.test.js +204 -0
- package/dist/beads/initialize.js +3 -0
- package/dist/beads/initialize.test.js +304 -0
- package/dist/beads/types.js +10 -0
- package/dist/cli/daemon-installer.js +225 -0
- package/dist/cli/daemon-installer.test.js +289 -0
- package/dist/cli/index.js +42 -0
- package/dist/cli/init-wizard.js +374 -0
- package/dist/cli/init-wizard.test.js +191 -0
- package/dist/config.js +385 -0
- package/dist/config.test.js +589 -0
- package/dist/cron/auto-tag.js +100 -0
- package/dist/cron/auto-tag.test.js +91 -0
- package/dist/cron/cadence.js +74 -0
- package/dist/cron/cadence.test.js +53 -0
- package/dist/cron/cron-sync-coordinator.js +66 -0
- package/dist/cron/cron-sync-coordinator.test.js +118 -0
- package/dist/cron/cron-sync.js +165 -0
- package/dist/cron/cron-sync.test.js +228 -0
- package/dist/cron/cron-tag-map-watcher.js +128 -0
- package/dist/cron/cron-tag-map-watcher.test.js +155 -0
- package/dist/cron/default-timezone.js +23 -0
- package/dist/cron/default-timezone.test.js +30 -0
- package/dist/cron/discord-sync.js +205 -0
- package/dist/cron/discord-sync.test.js +353 -0
- package/dist/cron/executor.js +303 -0
- package/dist/cron/executor.test.js +614 -0
- package/dist/cron/forum-sync.js +347 -0
- package/dist/cron/forum-sync.test.js +539 -0
- package/dist/cron/job-lock.js +164 -0
- package/dist/cron/job-lock.test.js +178 -0
- package/dist/cron/parser.js +68 -0
- package/dist/cron/parser.test.js +115 -0
- package/dist/cron/run-control.js +24 -0
- package/dist/cron/run-control.test.js +27 -0
- package/dist/cron/run-stats.js +265 -0
- package/dist/cron/run-stats.test.js +160 -0
- package/dist/cron/scheduler.js +97 -0
- package/dist/cron/scheduler.test.js +112 -0
- package/dist/cron/tag-map.js +47 -0
- package/dist/cron/tag-map.test.js +64 -0
- package/dist/cron/types.js +1 -0
- package/dist/discoclaw-plan-format.test.js +137 -0
- package/dist/discoclaw-recipe-format.test.js +137 -0
- package/dist/discord/abort-registry.js +70 -0
- package/dist/discord/action-categories.js +36 -0
- package/dist/discord/action-types.js +1 -0
- package/dist/discord/action-utils.js +58 -0
- package/dist/discord/action-utils.test.js +58 -0
- package/dist/discord/actions-beads.js +1 -0
- package/dist/discord/actions-beads.test.js +372 -0
- package/dist/discord/actions-bot-profile.js +107 -0
- package/dist/discord/actions-bot-profile.test.js +138 -0
- package/dist/discord/actions-channels.js +427 -0
- package/dist/discord/actions-channels.test.js +697 -0
- package/dist/discord/actions-config.js +173 -0
- package/dist/discord/actions-config.test.js +322 -0
- package/dist/discord/actions-crons.js +586 -0
- package/dist/discord/actions-crons.test.js +499 -0
- package/dist/discord/actions-defer.js +60 -0
- package/dist/discord/actions-defer.test.js +134 -0
- package/dist/discord/actions-forge.js +134 -0
- package/dist/discord/actions-forge.test.js +206 -0
- package/dist/discord/actions-guild.js +301 -0
- package/dist/discord/actions-guild.test.js +386 -0
- package/dist/discord/actions-memory.js +106 -0
- package/dist/discord/actions-memory.test.js +248 -0
- package/dist/discord/actions-messaging.js +401 -0
- package/dist/discord/actions-messaging.test.js +738 -0
- package/dist/discord/actions-moderation.js +65 -0
- package/dist/discord/actions-moderation.test.js +88 -0
- package/dist/discord/actions-plan.js +445 -0
- package/dist/discord/actions-plan.test.js +610 -0
- package/dist/discord/actions-poll.js +38 -0
- package/dist/discord/actions-poll.test.js +93 -0
- package/dist/discord/actions-tasks.js +3 -0
- package/dist/discord/actions-tasks.test.js +418 -0
- package/dist/discord/actions.js +600 -0
- package/dist/discord/actions.test.js +522 -0
- package/dist/discord/allowed-mentions.js +3 -0
- package/dist/discord/allowed-mentions.test.js +17 -0
- package/dist/discord/allowlist.js +29 -0
- package/dist/discord/allowlist.test.js +24 -0
- package/dist/discord/audit-handler.js +191 -0
- package/dist/discord/audit-handler.test.js +361 -0
- package/dist/discord/bot.js +141 -0
- package/dist/discord/channel-context.js +181 -0
- package/dist/discord/defer-scheduler.js +45 -0
- package/dist/discord/destructive-confirmation.js +128 -0
- package/dist/discord/destructive-confirmation.test.js +49 -0
- package/dist/discord/discord-plan-auto-implement.test.js +18 -0
- package/dist/discord/durable-memory.js +145 -0
- package/dist/discord/durable-memory.test.js +281 -0
- package/dist/discord/durable-write-queue.js +4 -0
- package/dist/discord/file-download.js +308 -0
- package/dist/discord/file-download.test.js +303 -0
- package/dist/discord/forge-audit-verdict.js +140 -0
- package/dist/discord/forge-auto-implement.js +80 -0
- package/dist/discord/forge-auto-implement.test.js +110 -0
- package/dist/discord/forge-commands.js +698 -0
- package/dist/discord/forge-commands.test.js +1606 -0
- package/dist/discord/forge-plan-registry.js +68 -0
- package/dist/discord/forge-plan-registry.test.js +127 -0
- package/dist/discord/forum-count-sync.js +130 -0
- package/dist/discord/forum-count-sync.test.js +200 -0
- package/dist/discord/health-command.js +98 -0
- package/dist/discord/health-command.test.js +195 -0
- package/dist/discord/help-command.js +22 -0
- package/dist/discord/help-command.test.js +49 -0
- package/dist/discord/image-download.js +201 -0
- package/dist/discord/image-download.test.js +499 -0
- package/dist/discord/inflight-replies.js +228 -0
- package/dist/discord/inflight-replies.test.js +295 -0
- package/dist/discord/json-extract.js +110 -0
- package/dist/discord/keyed-queue.js +22 -0
- package/dist/discord/memory-commands.js +85 -0
- package/dist/discord/memory-commands.test.js +159 -0
- package/dist/discord/memory-timing.integration.test.js +159 -0
- package/dist/discord/message-coordinator.js +2347 -0
- package/dist/discord/message-coordinator.onboarding.test.js +183 -0
- package/dist/discord/message-coordinator.plan-run.test.js +264 -0
- package/dist/discord/message-history.js +53 -0
- package/dist/discord/message-history.test.js +95 -0
- package/dist/discord/models-command.js +59 -0
- package/dist/discord/models-command.test.js +150 -0
- package/dist/discord/nickname.test.js +76 -0
- package/dist/discord/onboarding-completion.js +55 -0
- package/dist/discord/onboarding-completion.test.js +176 -0
- package/dist/discord/output-common.js +178 -0
- package/dist/discord/output-common.test.js +198 -0
- package/dist/discord/output-utils.js +156 -0
- package/dist/discord/parse-identity-name.test.js +129 -0
- package/dist/discord/plan-commands.js +612 -0
- package/dist/discord/plan-commands.test.js +1622 -0
- package/dist/discord/plan-manager.js +1491 -0
- package/dist/discord/plan-manager.test.js +2380 -0
- package/dist/discord/plan-parser.js +110 -0
- package/dist/discord/plan-parser.test.js +63 -0
- package/dist/discord/plan-run-phase-start.js +20 -0
- package/dist/discord/plan-run-phase-start.test.js +29 -0
- package/dist/discord/platform-message.js +45 -0
- package/dist/discord/platform-message.test.js +110 -0
- package/dist/discord/prompt-common.js +240 -0
- package/dist/discord/prompt-common.test.js +423 -0
- package/dist/discord/reaction-handler.js +691 -0
- package/dist/discord/reaction-handler.test.js +1574 -0
- package/dist/discord/reaction-prompts.js +118 -0
- package/dist/discord/reaction-prompts.test.js +253 -0
- package/dist/discord/reply-reference.js +66 -0
- package/dist/discord/reply-reference.test.js +125 -0
- package/dist/discord/restart-command.js +143 -0
- package/dist/discord/restart-command.test.js +196 -0
- package/dist/discord/runtime-utils.js +43 -0
- package/dist/discord/runtime-utils.test.js +112 -0
- package/dist/discord/session-key.js +7 -0
- package/dist/discord/session-key.test.js +13 -0
- package/dist/discord/shortterm-memory.js +166 -0
- package/dist/discord/shortterm-memory.test.js +345 -0
- package/dist/discord/shutdown-context.js +122 -0
- package/dist/discord/shutdown-context.test.js +279 -0
- package/dist/discord/startup-profile.test.js +214 -0
- package/dist/discord/status-channel.js +190 -0
- package/dist/discord/status-channel.test.js +282 -0
- package/dist/discord/status-command.js +206 -0
- package/dist/discord/status-command.test.js +341 -0
- package/dist/discord/streaming-progress.js +107 -0
- package/dist/discord/streaming-progress.test.js +93 -0
- package/dist/discord/summarizer.js +89 -0
- package/dist/discord/summarizer.test.js +245 -0
- package/dist/discord/system-bootstrap.js +396 -0
- package/dist/discord/system-bootstrap.test.js +724 -0
- package/dist/discord/thread-context.js +169 -0
- package/dist/discord/thread-context.test.js +386 -0
- package/dist/discord/tool-aware-queue.js +116 -0
- package/dist/discord/tool-aware-queue.test.js +180 -0
- package/dist/discord/update-command.js +127 -0
- package/dist/discord/update-command.test.js +275 -0
- package/dist/discord/user-errors.js +40 -0
- package/dist/discord/user-errors.test.js +31 -0
- package/dist/discord/user-turn-to-durable.js +111 -0
- package/dist/discord/user-turn-to-durable.test.js +273 -0
- package/dist/discord-followup.test.js +677 -0
- package/dist/discord.channel-context.test.js +95 -0
- package/dist/discord.fail-closed.test.js +199 -0
- package/dist/discord.health-command.integration.test.js +140 -0
- package/dist/discord.js +190 -0
- package/dist/discord.prompt-context.test.js +1431 -0
- package/dist/discord.render.test.js +621 -0
- package/dist/discord.status-wiring.test.js +187 -0
- package/dist/engine/claudeCli.js +137 -0
- package/dist/engine/types.js +1 -0
- package/dist/group-queue.js +25 -0
- package/dist/health/credential-check.js +175 -0
- package/dist/health/credential-check.test.js +401 -0
- package/dist/health/startup-healing.js +139 -0
- package/dist/health/startup-healing.test.js +298 -0
- package/dist/identity.js +36 -0
- package/dist/index.js +1378 -0
- package/dist/logging/logger-like.js +1 -0
- package/dist/observability/memory-sampler.js +51 -0
- package/dist/observability/memory-sampler.test.js +93 -0
- package/dist/observability/metrics.js +88 -0
- package/dist/observability/metrics.test.js +42 -0
- package/dist/onboarding/onboarding-flow.js +246 -0
- package/dist/onboarding/onboarding-flow.test.js +238 -0
- package/dist/onboarding/onboarding-writer.js +102 -0
- package/dist/onboarding/onboarding-writer.test.js +143 -0
- package/dist/pidlock.js +187 -0
- package/dist/pidlock.test.js +128 -0
- package/dist/pipeline/engine.js +206 -0
- package/dist/pipeline/engine.test.js +771 -0
- package/dist/root-policy.js +21 -0
- package/dist/root-policy.test.js +55 -0
- package/dist/runtime/claude-code-cli.js +35 -0
- package/dist/runtime/claude-code-cli.test.js +1199 -0
- package/dist/runtime/cli-adapter.js +584 -0
- package/dist/runtime/cli-output-parsers.js +108 -0
- package/dist/runtime/cli-shared.js +96 -0
- package/dist/runtime/cli-shared.test.js +104 -0
- package/dist/runtime/cli-strategy.js +6 -0
- package/dist/runtime/codex-cli.js +16 -0
- package/dist/runtime/codex-cli.test.js +862 -0
- package/dist/runtime/concurrency-limit.js +80 -0
- package/dist/runtime/concurrency-limit.test.js +137 -0
- package/dist/runtime/gemini-cli.js +16 -0
- package/dist/runtime/gemini-cli.test.js +413 -0
- package/dist/runtime/long-running-process.js +415 -0
- package/dist/runtime/long-running-process.test.js +318 -0
- package/dist/runtime/model-smoke-helpers.js +160 -0
- package/dist/runtime/model-smoke.test.js +194 -0
- package/dist/runtime/model-tiers.js +33 -0
- package/dist/runtime/model-tiers.test.js +65 -0
- package/dist/runtime/openai-auth.js +151 -0
- package/dist/runtime/openai-auth.test.js +361 -0
- package/dist/runtime/openai-compat.js +178 -0
- package/dist/runtime/openai-compat.test.js +449 -0
- package/dist/runtime/process-pool.js +93 -0
- package/dist/runtime/process-pool.test.js +148 -0
- package/dist/runtime/registry.js +15 -0
- package/dist/runtime/registry.test.js +47 -0
- package/dist/runtime/session-scanner.js +186 -0
- package/dist/runtime/session-scanner.test.js +257 -0
- package/dist/runtime/strategies/claude-strategy.js +193 -0
- package/dist/runtime/strategies/codex-strategy.js +161 -0
- package/dist/runtime/strategies/gemini-strategy.js +64 -0
- package/dist/runtime/strategies/template-strategy.js +85 -0
- package/dist/runtime/tool-capabilities.js +27 -0
- package/dist/runtime/tool-capabilities.test.js +24 -0
- package/dist/runtime/tool-labels.js +48 -0
- package/dist/runtime/types.js +2 -0
- package/dist/sessionManager.js +47 -0
- package/dist/sessions.js +18 -0
- package/dist/tasks/architecture-contract.js +33 -0
- package/dist/tasks/architecture-contract.test.js +90 -0
- package/dist/tasks/auto-tag.js +50 -0
- package/dist/tasks/auto-tag.test.js +64 -0
- package/dist/tasks/bd-cli.js +164 -0
- package/dist/tasks/bd-cli.test.js +359 -0
- package/dist/tasks/bead-sync.js +1 -0
- package/dist/tasks/context-summary.js +27 -0
- package/dist/tasks/discord-sync.js +3 -0
- package/dist/tasks/discord-sync.test.js +685 -0
- package/dist/tasks/discord-types.js +4 -0
- package/dist/tasks/find-task-by-thread.test.js +36 -0
- package/dist/tasks/forum-guard.js +81 -0
- package/dist/tasks/forum-guard.test.js +192 -0
- package/dist/tasks/initialize.js +77 -0
- package/dist/tasks/initialize.test.js +263 -0
- package/dist/tasks/logger-types.js +1 -0
- package/dist/tasks/metrics-types.js +3 -0
- package/dist/tasks/migrate.js +33 -0
- package/dist/tasks/migrate.test.js +156 -0
- package/dist/tasks/path-defaults.js +67 -0
- package/dist/tasks/path-defaults.test.js +73 -0
- package/dist/tasks/runtime-types.js +1 -0
- package/dist/tasks/service.js +33 -0
- package/dist/tasks/service.test.js +51 -0
- package/dist/tasks/store.js +238 -0
- package/dist/tasks/store.test.js +417 -0
- package/dist/tasks/sync-context.js +1 -0
- package/dist/tasks/sync-contract.js +24 -0
- package/dist/tasks/sync-contract.test.js +25 -0
- package/dist/tasks/sync-coordinator-metrics.js +41 -0
- package/dist/tasks/sync-coordinator-retries.js +71 -0
- package/dist/tasks/sync-coordinator.js +96 -0
- package/dist/tasks/sync-coordinator.test.js +501 -0
- package/dist/tasks/sync-types.js +1 -0
- package/dist/tasks/sync-watcher.js +27 -0
- package/dist/tasks/sync-watcher.test.js +92 -0
- package/dist/tasks/tag-map.js +36 -0
- package/dist/tasks/tag-map.test.js +54 -0
- package/dist/tasks/task-action-contract.js +16 -0
- package/dist/tasks/task-action-contract.test.js +16 -0
- package/dist/tasks/task-action-executor.js +18 -0
- package/dist/tasks/task-action-executor.test.js +420 -0
- package/dist/tasks/task-action-mutation-helpers.js +17 -0
- package/dist/tasks/task-action-mutations.js +151 -0
- package/dist/tasks/task-action-prompt.js +62 -0
- package/dist/tasks/task-action-read-ops.js +73 -0
- package/dist/tasks/task-action-runner-types.js +1 -0
- package/dist/tasks/task-action-thread-sync.js +82 -0
- package/dist/tasks/task-actions.js +3 -0
- package/dist/tasks/task-cli.js +227 -0
- package/dist/tasks/task-context.js +1 -0
- package/dist/tasks/task-lifecycle.js +46 -0
- package/dist/tasks/task-lifecycle.test.js +35 -0
- package/dist/tasks/task-sync-apply-plan.js +95 -0
- package/dist/tasks/task-sync-apply-types.js +12 -0
- package/dist/tasks/task-sync-apply.js +319 -0
- package/dist/tasks/task-sync-cli.js +89 -0
- package/dist/tasks/task-sync-cli.test.js +70 -0
- package/dist/tasks/task-sync-engine.js +88 -0
- package/dist/tasks/task-sync-engine.test.js +934 -0
- package/dist/tasks/task-sync-phase-apply.js +171 -0
- package/dist/tasks/task-sync-pipeline.js +2 -0
- package/dist/tasks/task-sync-pipeline.test.js +265 -0
- package/dist/tasks/task-sync-reconcile-plan.js +182 -0
- package/dist/tasks/task-sync-reconcile.js +144 -0
- package/dist/tasks/task-sync.js +56 -0
- package/dist/tasks/task-sync.test.js +86 -0
- package/dist/tasks/thread-cache.js +42 -0
- package/dist/tasks/thread-cache.test.js +89 -0
- package/dist/tasks/thread-contracts.test.js +711 -0
- package/dist/tasks/thread-forum-ops.js +68 -0
- package/dist/tasks/thread-helpers.js +86 -0
- package/dist/tasks/thread-helpers.test.js +33 -0
- package/dist/tasks/thread-lifecycle-ops.js +144 -0
- package/dist/tasks/thread-ops-shared.js +21 -0
- package/dist/tasks/thread-ops.js +2 -0
- package/dist/tasks/types.js +20 -0
- package/dist/tasks/types.test.js +60 -0
- package/dist/test-setup.js +11 -0
- package/dist/test-setup.test.js +42 -0
- package/dist/transport/types.js +1 -0
- package/dist/validate.js +41 -0
- package/dist/validate.test.js +94 -0
- package/dist/version.js +15 -0
- package/dist/version.test.js +31 -0
- package/dist/webhook/server.js +199 -0
- package/dist/webhook/server.test.js +460 -0
- package/dist/workspace-bootstrap.js +135 -0
- package/dist/workspace-bootstrap.test.js +514 -0
- package/dist/workspace-permissions.js +134 -0
- package/dist/workspace-permissions.test.js +181 -0
- package/package.json +74 -0
- package/scripts/cron/cron-tag-map.json +9 -0
- package/scripts/tasks/tag-map.json +10 -0
- package/systemd/discoclaw.service +19 -0
- package/templates/recipes/integration.discoclaw-recipe.md +171 -0
- package/templates/workspace/AGENTS.md +217 -0
- package/templates/workspace/BOOTSTRAP.md +1 -0
- package/templates/workspace/HEARTBEAT.md +10 -0
- package/templates/workspace/IDENTITY.md +16 -0
- package/templates/workspace/MEMORY.md +24 -0
- package/templates/workspace/SOUL.md +52 -0
- package/templates/workspace/TOOLS.md +304 -0
- package/templates/workspace/USER.md +37 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { loadDurableMemory, saveDurableMemory, addItem } from './durable-memory.js';
|
|
2
|
+
import { durableWriteQueue } from './durable-write-queue.js';
|
|
3
|
+
import { extractFirstJsonValue } from './json-extract.js';
|
|
4
|
+
const VALID_KINDS = new Set([
|
|
5
|
+
'preference', 'fact', 'project', 'constraint', 'person', 'tool', 'workflow',
|
|
6
|
+
]);
|
|
7
|
+
export const EXTRACTION_PROMPT = `You are a long-term memory extractor. Given a user message, decide whether it contains anything worth remembering permanently, and if so extract up to 3 items.
|
|
8
|
+
|
|
9
|
+
## One-month test
|
|
10
|
+
Only extract something if it would still be useful to know in a month. Most messages contain nothing worth storing — return [] liberally.
|
|
11
|
+
|
|
12
|
+
## KEEP — stable, lasting facts
|
|
13
|
+
- User preferences and opinions (editor, language, style, communication preferences)
|
|
14
|
+
- Personal facts (name, location, timezone, family, pets, hobbies, job title)
|
|
15
|
+
- Stable project context (project names, tech stacks, team structure, architecture decisions)
|
|
16
|
+
- Cross-session conventions and workflows (branching strategy, deploy process, naming conventions)
|
|
17
|
+
- Relationships between people, teams, or projects
|
|
18
|
+
|
|
19
|
+
## EXCLUDE — transient task state
|
|
20
|
+
- Current bugs being fixed, PRs in flight, features being built right now
|
|
21
|
+
- One-time setup steps or installation instructions
|
|
22
|
+
- Transient decisions that will resolve within days
|
|
23
|
+
- Specific code line numbers, file paths, commit hashes, or error messages
|
|
24
|
+
- Anything that reads like a status update rather than a lasting fact
|
|
25
|
+
- In-progress test gaps or TODO items
|
|
26
|
+
- Summaries of what was just done in the current session
|
|
27
|
+
|
|
28
|
+
## Output format
|
|
29
|
+
Return a JSON array of objects with "kind" and "text" fields. Valid kinds: preference, fact, project, constraint, person, tool, workflow. Max 3 items. If nothing passes the one-month test, return [].
|
|
30
|
+
|
|
31
|
+
Only extract information the user explicitly stated.
|
|
32
|
+
|
|
33
|
+
User message:
|
|
34
|
+
{userMessage}
|
|
35
|
+
|
|
36
|
+
JSON array:`;
|
|
37
|
+
const MAX_ITEMS_PER_EXTRACTION = 3;
|
|
38
|
+
export async function extractFromUserTurn(runtime, opts) {
|
|
39
|
+
const prompt = EXTRACTION_PROMPT.replace('{userMessage}', opts.userMessageText);
|
|
40
|
+
let finalText = '';
|
|
41
|
+
let deltaText = '';
|
|
42
|
+
for await (const evt of runtime.invoke({
|
|
43
|
+
prompt,
|
|
44
|
+
model: opts.model,
|
|
45
|
+
cwd: opts.cwd,
|
|
46
|
+
tools: [],
|
|
47
|
+
timeoutMs: opts.timeoutMs ?? 15_000,
|
|
48
|
+
})) {
|
|
49
|
+
if (evt.type === 'text_final') {
|
|
50
|
+
finalText = evt.text;
|
|
51
|
+
}
|
|
52
|
+
else if (evt.type === 'text_delta') {
|
|
53
|
+
deltaText += evt.text;
|
|
54
|
+
}
|
|
55
|
+
else if (evt.type === 'error') {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const raw = (finalText || deltaText).trim();
|
|
60
|
+
return parseExtractionResult(raw);
|
|
61
|
+
}
|
|
62
|
+
export function parseExtractionResult(raw) {
|
|
63
|
+
try {
|
|
64
|
+
const jsonArray = extractFirstJsonValue(raw, { arrayOnly: true });
|
|
65
|
+
if (!jsonArray)
|
|
66
|
+
return [];
|
|
67
|
+
const parsed = JSON.parse(jsonArray);
|
|
68
|
+
if (!Array.isArray(parsed))
|
|
69
|
+
return [];
|
|
70
|
+
return parsed
|
|
71
|
+
.filter((item) => item &&
|
|
72
|
+
typeof item === 'object' &&
|
|
73
|
+
typeof item.text === 'string' &&
|
|
74
|
+
item.text.trim().length > 0 &&
|
|
75
|
+
VALID_KINDS.has(item.kind))
|
|
76
|
+
.map((item) => ({ kind: item.kind, text: item.text.trim() }))
|
|
77
|
+
.slice(0, MAX_ITEMS_PER_EXTRACTION);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function applyUserTurnToDurable(opts) {
|
|
84
|
+
const items = await extractFromUserTurn(opts.runtime, {
|
|
85
|
+
userMessageText: opts.userMessageText,
|
|
86
|
+
model: opts.model,
|
|
87
|
+
cwd: opts.cwd,
|
|
88
|
+
});
|
|
89
|
+
if (items.length === 0)
|
|
90
|
+
return;
|
|
91
|
+
await durableWriteQueue.run(opts.userId, async () => {
|
|
92
|
+
const store = (await loadDurableMemory(opts.durableDataDir, opts.userId)) ?? {
|
|
93
|
+
version: 1,
|
|
94
|
+
updatedAt: 0,
|
|
95
|
+
items: [],
|
|
96
|
+
};
|
|
97
|
+
for (const item of items) {
|
|
98
|
+
const source = { type: 'summary' };
|
|
99
|
+
if (opts.channelId)
|
|
100
|
+
source.channelId = opts.channelId;
|
|
101
|
+
if (opts.messageId)
|
|
102
|
+
source.messageId = opts.messageId;
|
|
103
|
+
if (opts.guildId)
|
|
104
|
+
source.guildId = opts.guildId;
|
|
105
|
+
if (opts.channelName)
|
|
106
|
+
source.channelName = opts.channelName;
|
|
107
|
+
addItem(store, item.text, { ...source }, opts.durableMaxItems, item.kind);
|
|
108
|
+
}
|
|
109
|
+
await saveDurableMemory(opts.durableDataDir, opts.userId, store);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parseExtractionResult, applyUserTurnToDurable, EXTRACTION_PROMPT } from './user-turn-to-durable.js';
|
|
6
|
+
import { loadDurableMemory, addItem, saveDurableMemory } from './durable-memory.js';
|
|
7
|
+
async function makeTmpDir() {
|
|
8
|
+
return fs.mkdtemp(path.join(os.tmpdir(), 'user-turn-durable-test-'));
|
|
9
|
+
}
|
|
10
|
+
function makeRuntime(responseText) {
|
|
11
|
+
return {
|
|
12
|
+
invoke: async function* () {
|
|
13
|
+
yield { type: 'text_final', text: responseText };
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
describe('parseExtractionResult', () => {
|
|
18
|
+
it('parses valid JSON array', () => {
|
|
19
|
+
const raw = '[{"kind":"fact","text":"Likes TypeScript"},{"kind":"preference","text":"Prefers dark mode"}]';
|
|
20
|
+
const items = parseExtractionResult(raw);
|
|
21
|
+
expect(items).toEqual([
|
|
22
|
+
{ kind: 'fact', text: 'Likes TypeScript' },
|
|
23
|
+
{ kind: 'preference', text: 'Prefers dark mode' },
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
it('returns empty on malformed JSON', () => {
|
|
27
|
+
expect(parseExtractionResult('not json at all')).toEqual([]);
|
|
28
|
+
expect(parseExtractionResult('{}')).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
it('filters out invalid kinds', () => {
|
|
31
|
+
const raw = '[{"kind":"invalid","text":"should be dropped"},{"kind":"fact","text":"kept"}]';
|
|
32
|
+
const items = parseExtractionResult(raw);
|
|
33
|
+
expect(items).toEqual([{ kind: 'fact', text: 'kept' }]);
|
|
34
|
+
});
|
|
35
|
+
it('filters out items with empty text', () => {
|
|
36
|
+
const raw = '[{"kind":"fact","text":""},{"kind":"fact","text":" "},{"kind":"fact","text":"real"}]';
|
|
37
|
+
const items = parseExtractionResult(raw);
|
|
38
|
+
expect(items).toEqual([{ kind: 'fact', text: 'real' }]);
|
|
39
|
+
});
|
|
40
|
+
it('enforces cap of 3 items even if model returns more', () => {
|
|
41
|
+
const many = Array.from({ length: 10 }, (_, i) => ({ kind: 'fact', text: `Item ${i}` }));
|
|
42
|
+
const raw = JSON.stringify(many);
|
|
43
|
+
const items = parseExtractionResult(raw);
|
|
44
|
+
expect(items).toHaveLength(3);
|
|
45
|
+
});
|
|
46
|
+
it('handles JSON inside markdown fences', () => {
|
|
47
|
+
const raw = '```json\n[{"kind":"fact","text":"Extracted"}]\n```';
|
|
48
|
+
const items = parseExtractionResult(raw);
|
|
49
|
+
expect(items).toEqual([{ kind: 'fact', text: 'Extracted' }]);
|
|
50
|
+
});
|
|
51
|
+
it('returns empty array response', () => {
|
|
52
|
+
expect(parseExtractionResult('[]')).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
it('returns empty for non-array JSON object', () => {
|
|
55
|
+
expect(parseExtractionResult('{"key":"val"}')).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
it('extracts first array when trailing brackets exist', () => {
|
|
58
|
+
const raw = '[{"kind":"fact","text":"ok"}] some text [more stuff]';
|
|
59
|
+
const items = parseExtractionResult(raw);
|
|
60
|
+
expect(items).toEqual([{ kind: 'fact', text: 'ok' }]);
|
|
61
|
+
});
|
|
62
|
+
it('skips non-array json values and finds the first valid array', () => {
|
|
63
|
+
const raw = '{"note":"not an array"}\n[{"kind":"fact","text":"kept"}]';
|
|
64
|
+
const items = parseExtractionResult(raw);
|
|
65
|
+
expect(items).toEqual([{ kind: 'fact', text: 'kept' }]);
|
|
66
|
+
});
|
|
67
|
+
it('parses array from json fence with surrounding prose', () => {
|
|
68
|
+
const raw = 'Result below:\n```json\n[{"kind":"workflow","text":"Use squash merges"}]\n```\nThanks!';
|
|
69
|
+
const items = parseExtractionResult(raw);
|
|
70
|
+
expect(items).toEqual([{ kind: 'workflow', text: 'Use squash merges' }]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('EXTRACTION_PROMPT', () => {
|
|
74
|
+
it('is exported', () => {
|
|
75
|
+
expect(typeof EXTRACTION_PROMPT).toBe('string');
|
|
76
|
+
expect(EXTRACTION_PROMPT.length).toBeGreaterThan(0);
|
|
77
|
+
});
|
|
78
|
+
it('contains the one-month test heuristic', () => {
|
|
79
|
+
expect(EXTRACTION_PROMPT).toContain('one-month test');
|
|
80
|
+
expect(EXTRACTION_PROMPT).toContain('still be useful to know in a month');
|
|
81
|
+
});
|
|
82
|
+
it('includes keep criteria for preferences and personal facts', () => {
|
|
83
|
+
expect(EXTRACTION_PROMPT).toContain('preferences');
|
|
84
|
+
expect(EXTRACTION_PROMPT).toContain('Personal facts');
|
|
85
|
+
expect(EXTRACTION_PROMPT).toContain('Stable project context');
|
|
86
|
+
expect(EXTRACTION_PROMPT).toContain('Cross-session conventions');
|
|
87
|
+
});
|
|
88
|
+
it('includes exclude criteria for transient task state', () => {
|
|
89
|
+
expect(EXTRACTION_PROMPT).toContain('bugs being fixed');
|
|
90
|
+
expect(EXTRACTION_PROMPT).toContain('PRs in flight');
|
|
91
|
+
expect(EXTRACTION_PROMPT).toContain('One-time setup steps');
|
|
92
|
+
expect(EXTRACTION_PROMPT).toContain('commit hashes');
|
|
93
|
+
expect(EXTRACTION_PROMPT).toContain('status update');
|
|
94
|
+
});
|
|
95
|
+
it('preserves the JSON output contract', () => {
|
|
96
|
+
expect(EXTRACTION_PROMPT).toContain('"kind"');
|
|
97
|
+
expect(EXTRACTION_PROMPT).toContain('"text"');
|
|
98
|
+
expect(EXTRACTION_PROMPT).toContain('preference, fact, project, constraint, person, tool, workflow');
|
|
99
|
+
expect(EXTRACTION_PROMPT).toContain('Max 3 items');
|
|
100
|
+
expect(EXTRACTION_PROMPT).toContain('return []');
|
|
101
|
+
});
|
|
102
|
+
it('contains the {userMessage} placeholder', () => {
|
|
103
|
+
expect(EXTRACTION_PROMPT).toContain('{userMessage}');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('applyUserTurnToDurable', () => {
|
|
107
|
+
it('writes extracted items to durable store', async () => {
|
|
108
|
+
const dir = await makeTmpDir();
|
|
109
|
+
const runtime = makeRuntime('[{"kind":"fact","text":"Likes cats"}]');
|
|
110
|
+
await applyUserTurnToDurable({
|
|
111
|
+
runtime,
|
|
112
|
+
userMessageText: 'I love cats',
|
|
113
|
+
userId: '42',
|
|
114
|
+
durableDataDir: dir,
|
|
115
|
+
durableMaxItems: 200,
|
|
116
|
+
model: 'haiku',
|
|
117
|
+
cwd: '/tmp',
|
|
118
|
+
});
|
|
119
|
+
const store = await loadDurableMemory(dir, '42');
|
|
120
|
+
expect(store).not.toBeNull();
|
|
121
|
+
expect(store.items).toHaveLength(1);
|
|
122
|
+
expect(store.items[0].text).toBe('Likes cats');
|
|
123
|
+
expect(store.items[0].kind).toBe('fact');
|
|
124
|
+
expect(store.items[0].source.type).toBe('summary');
|
|
125
|
+
});
|
|
126
|
+
it('does not duplicate existing items', async () => {
|
|
127
|
+
const dir = await makeTmpDir();
|
|
128
|
+
// Pre-seed a store with an existing item.
|
|
129
|
+
const existing = { version: 1, updatedAt: 0, items: [] };
|
|
130
|
+
addItem(existing, 'Likes cats', { type: 'summary' }, 200, 'fact');
|
|
131
|
+
await saveDurableMemory(dir, '42', existing);
|
|
132
|
+
const runtime = makeRuntime('[{"kind":"fact","text":"Likes cats"}]');
|
|
133
|
+
await applyUserTurnToDurable({
|
|
134
|
+
runtime,
|
|
135
|
+
userMessageText: 'I love cats',
|
|
136
|
+
userId: '42',
|
|
137
|
+
durableDataDir: dir,
|
|
138
|
+
durableMaxItems: 200,
|
|
139
|
+
model: 'haiku',
|
|
140
|
+
cwd: '/tmp',
|
|
141
|
+
});
|
|
142
|
+
const store = await loadDurableMemory(dir, '42');
|
|
143
|
+
expect(store.items).toHaveLength(1); // Still just 1, not 2.
|
|
144
|
+
});
|
|
145
|
+
it('respects maxItems cap', async () => {
|
|
146
|
+
const dir = await makeTmpDir();
|
|
147
|
+
// Pre-seed store at cap.
|
|
148
|
+
const existing = { version: 1, updatedAt: 0, items: [] };
|
|
149
|
+
addItem(existing, 'Item 1', { type: 'manual' }, 2, 'fact');
|
|
150
|
+
addItem(existing, 'Item 2', { type: 'manual' }, 2, 'fact');
|
|
151
|
+
await saveDurableMemory(dir, '42', existing);
|
|
152
|
+
const runtime = makeRuntime('[{"kind":"preference","text":"New preference"}]');
|
|
153
|
+
await applyUserTurnToDurable({
|
|
154
|
+
runtime,
|
|
155
|
+
userMessageText: 'Some message',
|
|
156
|
+
userId: '42',
|
|
157
|
+
durableDataDir: dir,
|
|
158
|
+
durableMaxItems: 2,
|
|
159
|
+
model: 'haiku',
|
|
160
|
+
cwd: '/tmp',
|
|
161
|
+
});
|
|
162
|
+
const store = await loadDurableMemory(dir, '42');
|
|
163
|
+
expect(store.items).toHaveLength(2); // Cap enforced.
|
|
164
|
+
expect(store.items.some((it) => it.text === 'New preference')).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
it('handles runtime returning empty array gracefully', async () => {
|
|
167
|
+
const dir = await makeTmpDir();
|
|
168
|
+
const runtime = makeRuntime('[]');
|
|
169
|
+
await applyUserTurnToDurable({
|
|
170
|
+
runtime,
|
|
171
|
+
userMessageText: 'Nothing notable here',
|
|
172
|
+
userId: '42',
|
|
173
|
+
durableDataDir: dir,
|
|
174
|
+
durableMaxItems: 200,
|
|
175
|
+
model: 'haiku',
|
|
176
|
+
cwd: '/tmp',
|
|
177
|
+
});
|
|
178
|
+
// No file should be created — nothing to write.
|
|
179
|
+
const store = await loadDurableMemory(dir, '42');
|
|
180
|
+
expect(store).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
it('concurrent calls for same user serialize correctly', async () => {
|
|
183
|
+
const dir = await makeTmpDir();
|
|
184
|
+
// Each invocation extracts a different fact.
|
|
185
|
+
let callCount = 0;
|
|
186
|
+
const runtime = {
|
|
187
|
+
invoke: async function* () {
|
|
188
|
+
const n = ++callCount;
|
|
189
|
+
yield { type: 'text_final', text: `[{"kind":"fact","text":"Fact ${n}"}]` };
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
await Promise.all([
|
|
193
|
+
applyUserTurnToDurable({
|
|
194
|
+
runtime, userMessageText: 'msg1', userId: '42',
|
|
195
|
+
durableDataDir: dir, durableMaxItems: 200, model: 'haiku', cwd: '/tmp',
|
|
196
|
+
}),
|
|
197
|
+
applyUserTurnToDurable({
|
|
198
|
+
runtime, userMessageText: 'msg2', userId: '42',
|
|
199
|
+
durableDataDir: dir, durableMaxItems: 200, model: 'haiku', cwd: '/tmp',
|
|
200
|
+
}),
|
|
201
|
+
applyUserTurnToDurable({
|
|
202
|
+
runtime, userMessageText: 'msg3', userId: '42',
|
|
203
|
+
durableDataDir: dir, durableMaxItems: 200, model: 'haiku', cwd: '/tmp',
|
|
204
|
+
}),
|
|
205
|
+
]);
|
|
206
|
+
const store = await loadDurableMemory(dir, '42');
|
|
207
|
+
expect(store).not.toBeNull();
|
|
208
|
+
// All 3 facts should be stored (no overwrites from races).
|
|
209
|
+
expect(store.items).toHaveLength(3);
|
|
210
|
+
});
|
|
211
|
+
it('persists Discord metadata in source when provided', async () => {
|
|
212
|
+
const dir = await makeTmpDir();
|
|
213
|
+
const runtime = makeRuntime('[{"kind":"fact","text":"Likes cats"}]');
|
|
214
|
+
await applyUserTurnToDurable({
|
|
215
|
+
runtime,
|
|
216
|
+
userMessageText: 'I love cats',
|
|
217
|
+
userId: '42',
|
|
218
|
+
durableDataDir: dir,
|
|
219
|
+
durableMaxItems: 200,
|
|
220
|
+
model: 'haiku',
|
|
221
|
+
cwd: '/tmp',
|
|
222
|
+
channelId: 'ch1',
|
|
223
|
+
messageId: 'msg1',
|
|
224
|
+
guildId: 'g1',
|
|
225
|
+
channelName: 'dev',
|
|
226
|
+
});
|
|
227
|
+
const store = await loadDurableMemory(dir, '42');
|
|
228
|
+
expect(store).not.toBeNull();
|
|
229
|
+
expect(store.items[0].source).toEqual({
|
|
230
|
+
type: 'summary',
|
|
231
|
+
channelId: 'ch1',
|
|
232
|
+
messageId: 'msg1',
|
|
233
|
+
guildId: 'g1',
|
|
234
|
+
channelName: 'dev',
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
it('omits Discord metadata when not provided', async () => {
|
|
238
|
+
const dir = await makeTmpDir();
|
|
239
|
+
const runtime = makeRuntime('[{"kind":"fact","text":"Likes cats"}]');
|
|
240
|
+
await applyUserTurnToDurable({
|
|
241
|
+
runtime,
|
|
242
|
+
userMessageText: 'I love cats',
|
|
243
|
+
userId: '42',
|
|
244
|
+
durableDataDir: dir,
|
|
245
|
+
durableMaxItems: 200,
|
|
246
|
+
model: 'haiku',
|
|
247
|
+
cwd: '/tmp',
|
|
248
|
+
});
|
|
249
|
+
const store = await loadDurableMemory(dir, '42');
|
|
250
|
+
expect(store).not.toBeNull();
|
|
251
|
+
expect(store.items[0].source).toEqual({ type: 'summary' });
|
|
252
|
+
});
|
|
253
|
+
it('handles runtime error gracefully (no crash)', async () => {
|
|
254
|
+
const dir = await makeTmpDir();
|
|
255
|
+
const runtime = {
|
|
256
|
+
invoke: async function* () {
|
|
257
|
+
yield { type: 'error', message: 'API error' };
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
// Should not throw.
|
|
261
|
+
await applyUserTurnToDurable({
|
|
262
|
+
runtime,
|
|
263
|
+
userMessageText: 'Something',
|
|
264
|
+
userId: '42',
|
|
265
|
+
durableDataDir: dir,
|
|
266
|
+
durableMaxItems: 200,
|
|
267
|
+
model: 'haiku',
|
|
268
|
+
cwd: '/tmp',
|
|
269
|
+
});
|
|
270
|
+
const store = await loadDurableMemory(dir, '42');
|
|
271
|
+
expect(store).toBeNull();
|
|
272
|
+
});
|
|
273
|
+
});
|