@pi-unipi/unipi 3.0.0-alpha.2 → 3.0.0-alpha.21
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 +292 -0
- package/README.md +8 -11
- package/docs/prefix-cache-architecture.md +14 -0
- package/package.json +22 -24
- package/packages/ask-user/README.md +55 -107
- package/packages/ask-user/ask-ui.ts +306 -732
- package/packages/ask-user/clipboard.ts +50 -0
- package/packages/ask-user/config.ts +74 -78
- package/packages/ask-user/package.json +2 -2
- package/packages/ask-user/questions.ts +151 -0
- package/packages/ask-user/skills/ask-user/SKILL.md +59 -133
- package/packages/ask-user/tools.ts +198 -432
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +38 -43
- package/packages/autocomplete/src/sorting.ts +8 -0
- package/packages/background-tasks/package.json +2 -2
- package/packages/background-tasks/src/config.ts +0 -73
- package/packages/background-tasks/src/index.ts +20 -16
- package/packages/background-tasks/src/registry.ts +1 -171
- package/packages/background-tasks/src/types.ts +0 -32
- package/packages/btw/README.md +13 -56
- package/packages/btw/extensions/btw.ts +403 -1796
- package/packages/btw/package.json +5 -2
- package/packages/btw/skills/btw/SKILL.md +7 -143
- package/packages/compactor/README.md +56 -287
- package/packages/compactor/package.json +5 -9
- package/packages/compactor/skills/compactor/SKILL.md +13 -20
- package/packages/compactor/src/card.ts +90 -0
- package/packages/compactor/src/commands/index.ts +73 -130
- package/packages/compactor/src/compaction/compact-args.ts +5 -3
- package/packages/compactor/src/compaction/filter-noise.ts +1 -1
- package/packages/compactor/src/compaction/hooks.ts +487 -311
- package/packages/compactor/src/compaction/source.ts +178 -0
- package/packages/compactor/src/compaction/summarize.ts +586 -52
- package/packages/compactor/src/config/manager.ts +130 -127
- package/packages/compactor/src/config/schema.ts +32 -48
- package/packages/compactor/src/index.ts +40 -431
- package/packages/compactor/src/info-screen.ts +44 -190
- package/packages/compactor/src/session/recall-blocks.ts +1 -1
- package/packages/compactor/src/stats.ts +76 -0
- package/packages/compactor/src/tools/context-budget.ts +17 -34
- package/packages/compactor/src/tools/ctx-doctor.ts +45 -36
- package/packages/compactor/src/tools/register.ts +29 -269
- package/packages/compactor/src/types.ts +47 -186
- package/packages/core/README.md +25 -0
- package/packages/core/child-agent.ts +1 -0
- package/packages/core/command-echo.ts +63 -0
- package/packages/core/compaction-context.ts +41 -0
- package/packages/core/constants.ts +8 -38
- package/packages/core/events.ts +0 -53
- package/packages/core/fusion-status.ts +4 -0
- package/packages/core/global.d.ts +10 -0
- package/packages/core/harness-messages.ts +392 -0
- package/packages/core/index.ts +14 -0
- package/packages/core/kanboard-status.ts +49 -0
- package/packages/core/long-horizon-owner-status.ts +46 -0
- package/packages/core/model-cache.ts +81 -7
- package/packages/core/package.json +6 -3
- package/packages/core/src/attach/detect.ts +163 -0
- package/packages/core/src/child-agent/child-args.ts +31 -0
- package/packages/core/src/child-agent/delivery.ts +41 -0
- package/packages/core/src/child-agent/index.ts +5 -0
- package/packages/core/src/child-agent/readers.ts +203 -0
- package/packages/core/src/child-agent/runtime.ts +584 -0
- package/packages/core/src/evidence.ts +78 -0
- package/packages/core/src/hints/browser.ts +278 -0
- package/packages/core/src/hints/crab-data.ts +19 -0
- package/packages/core/src/hints/index.ts +865 -0
- package/packages/core/src/hints/lines.ts +607 -0
- package/packages/core/src/hints/store.ts +120 -0
- package/packages/core/src/jev/chatter.ts +24 -0
- package/packages/core/src/jev/settings.ts +177 -14
- package/packages/core/src/package-colors.ts +2 -0
- package/packages/core/src/settings/catalog.ts +39 -15
- package/packages/core/src/settings/engine.ts +48 -2
- package/packages/core/src/settings/hub.ts +83 -20
- package/packages/core/src/settings/open.ts +49 -0
- package/packages/core/src/settings/schema.ts +27 -7
- package/packages/core/src/tui/hub-kit.ts +18 -5
- package/packages/core/src/tui/kit.ts +198 -0
- package/packages/core/src/tui/progress.ts +73 -0
- package/packages/core/src/turn/arbiter.ts +290 -0
- package/packages/footer/README.md +4 -6
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/glance-editor.ts +19 -11
- package/packages/footer/src/help.ts +1 -1
- package/packages/footer/src/index.ts +36 -3
- package/packages/footer/src/presets.ts +3 -4
- package/packages/footer/src/rendering/icons.ts +0 -20
- package/packages/footer/src/segments/compactor.ts +52 -142
- package/packages/footer/src/segments/status-ext.ts +0 -1
- package/packages/fusion/README.md +14 -4
- package/packages/fusion/package.json +4 -3
- package/packages/fusion/src/child-args.ts +1 -0
- package/packages/fusion/src/index.ts +161 -8
- package/packages/fusion/src/pi-settings.ts +57 -0
- package/packages/fusion/src/picker.ts +186 -51
- package/packages/fusion/src/preset.ts +56 -2
- package/packages/fusion/src/prompts.ts +1 -1
- package/packages/fusion/src/sidekick-runtime.ts +1 -420
- package/packages/fusion/src/tools.ts +107 -128
- package/packages/fusion/src/transcript.ts +27 -85
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/README.md +18 -30
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/input-shortcuts/src/burst.ts +88 -0
- package/packages/input-shortcuts/src/chord-overlay.ts +20 -70
- package/packages/input-shortcuts/src/cleared-input.ts +97 -0
- package/packages/input-shortcuts/src/index.ts +99 -193
- package/packages/input-shortcuts/src/last-response.ts +40 -0
- package/packages/input-shortcuts/src/registers.ts +8 -23
- package/packages/input-shortcuts/src/types.ts +4 -16
- package/packages/input-shortcuts/src/undo-redo.ts +59 -47
- package/packages/kanboard/README.md +51 -47
- package/packages/kanboard/index.ts +293 -55
- package/packages/kanboard/package.json +8 -8
- package/packages/kanboard/skills/kanboard/SKILL.md +61 -27
- package/packages/kanboard/src/badges.ts +93 -0
- package/packages/kanboard/src/bin.ts +4 -2
- package/packages/kanboard/src/commands.ts +218 -84
- package/packages/kanboard/src/debug.ts +20 -0
- package/packages/kanboard/src/guard.ts +267 -109
- package/packages/kanboard/src/monitor.ts +330 -0
- package/packages/kanboard/src/notice-buffer.ts +74 -0
- package/packages/kanboard/src/progress.ts +27 -0
- package/packages/kanboard/src/reminders.ts +234 -0
- package/packages/kanboard/src/settings.ts +69 -15
- package/packages/kanboard/src/shapes.ts +5 -0
- package/packages/long-horizon/index.ts +190 -13
- package/packages/long-horizon/package.json +3 -3
- package/packages/long-horizon/src/commands.ts +103 -7
- package/packages/long-horizon/src/compaction-brief.ts +35 -0
- package/packages/long-horizon/src/engine/continuation.ts +84 -15
- package/packages/long-horizon/src/engine/goal-state.ts +64 -13
- package/packages/long-horizon/src/engine/nudge-stash.ts +71 -0
- package/packages/long-horizon/src/engine/ralph.ts +3 -2
- package/packages/long-horizon/src/engine/runaway.ts +68 -11
- package/packages/long-horizon/src/engine/verifier.ts +19 -0
- package/packages/long-horizon/src/gate.ts +37 -12
- package/packages/long-horizon/src/judge/resolve.ts +1 -0
- package/packages/long-horizon/src/owner.ts +35 -2
- package/packages/long-horizon/src/progress.ts +87 -0
- package/packages/long-horizon/src/prompts/goal.ts +1 -1
- package/packages/long-horizon/src/runtime.ts +172 -18
- package/packages/long-horizon/src/settings.ts +54 -39
- package/packages/long-horizon/src/tools/goal.ts +6 -0
- package/packages/long-horizon/src/tools/graph.ts +1 -1
- package/packages/long-horizon/src/tools/swarm.ts +3 -3
- package/packages/mcp/package.json +2 -2
- package/packages/memory/commands.ts +7 -2
- package/packages/memory/index.ts +210 -32
- package/packages/memory/package.json +3 -3
- package/packages/memory/save-session.ts +264 -0
- package/packages/memory/settings.ts +17 -0
- package/packages/memory/tools.ts +253 -203
- package/packages/notify/package.json +2 -2
- package/packages/skill-registry/README.md +53 -16
- package/packages/skill-registry/index.ts +230 -0
- package/packages/skill-registry/package.json +19 -8
- package/packages/skill-registry/src/editor.ts +206 -0
- package/packages/skill-registry/src/judge.ts +289 -0
- package/packages/skill-registry/src/registry.ts +87 -0
- package/packages/skill-registry/src/settings.ts +259 -0
- package/packages/skill-registry/src/vault.ts +80 -0
- package/packages/subagents/README.md +39 -119
- package/packages/subagents/package.json +6 -13
- package/packages/subagents/skills/subagents/SKILL.md +18 -42
- package/packages/subagents/src/agents.ts +213 -0
- package/packages/subagents/src/cards.ts +182 -0
- package/packages/subagents/src/index.ts +609 -1084
- package/packages/subagents/src/manager.ts +429 -0
- package/packages/subagents/src/profiles.ts +130 -0
- package/packages/subagents/src/transcript.ts +154 -0
- package/packages/subagents/src/ui.ts +389 -0
- package/packages/unipi/bundled.js +39756 -45569
- package/packages/unipi/index.ts +55 -10
- package/packages/updater/package.json +2 -2
- package/packages/utility/README.md +76 -69
- package/packages/utility/package.json +2 -2
- package/packages/utility/skills/image/SKILL.md +37 -0
- package/packages/utility/skills/utility/SKILL.md +17 -60
- package/packages/utility/src/answer/extract.ts +128 -0
- package/packages/utility/src/answer/index.ts +213 -0
- package/packages/utility/src/answer/reply.ts +133 -0
- package/packages/utility/src/answer/web.ts +167 -0
- package/packages/utility/src/attach/index.ts +205 -0
- package/packages/utility/src/commands.ts +38 -142
- package/packages/utility/src/diagnostics/engine.ts +50 -5
- package/packages/utility/src/image/index.ts +33 -0
- package/packages/utility/src/image/models.ts +221 -0
- package/packages/{image/src → utility/src/image}/recognize.ts +2 -2
- package/packages/utility/src/image/settings.ts +178 -0
- package/packages/{image/src/image-source.ts → utility/src/image/source.ts} +1 -1
- package/packages/utility/src/image/tools.ts +168 -0
- package/packages/utility/src/image/transports.ts +186 -0
- package/packages/utility/src/index.ts +68 -389
- package/packages/utility/src/lifecycle/cleanup.ts +116 -311
- package/packages/utility/src/rename/gate.ts +102 -0
- package/packages/utility/src/rename/index.ts +191 -0
- package/packages/utility/src/rename/session.ts +170 -0
- package/packages/utility/src/render/delegated.ts +285 -0
- package/packages/utility/src/render/harness.ts +683 -0
- package/packages/utility/src/render/parse.ts +156 -0
- package/packages/utility/src/render/pi-settings.ts +38 -0
- package/packages/utility/src/render/reply-bg.ts +307 -0
- package/packages/utility/src/render/simple.ts +724 -0
- package/packages/utility/src/render/spacing.ts +235 -0
- package/packages/utility/src/render/styled.ts +251 -0
- package/packages/utility/src/render/tools.ts +353 -0
- package/packages/utility/src/settings.ts +166 -233
- package/packages/utility/src/types.ts +1 -222
- package/packages/watchdog/index.ts +17 -6
- package/packages/watchdog/package.json +3 -3
- package/packages/watchdog/src/config.ts +3 -2
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/README.md +3 -0
- package/packages/workflow/index.ts +0 -2
- package/packages/workflow/package.json +2 -2
- package/packages/workflow/src/permission/decide.ts +6 -4
- package/packages/workflow/src/permission/index.ts +17 -7
- package/packages/workflow/src/permission/settings.ts +39 -4
- package/packages/workflow/src/plan/index.ts +19 -4
- package/packages/background-tasks/extensions/delegate-child.ts +0 -1
- package/packages/background-tasks/src/context-parent-snapshot.ts +0 -142
- package/packages/background-tasks/src/context-token-budget.ts +0 -903
- package/packages/background-tasks/src/context-visible-conversation-v2.ts +0 -551
- package/packages/background-tasks/src/delegate/artifacts.ts +0 -487
- package/packages/background-tasks/src/delegate/budget.ts +0 -415
- package/packages/background-tasks/src/delegate/hook-contract-evidence.json +0 -18
- package/packages/background-tasks/src/delegate/hook-contract.ts +0 -154
- package/packages/background-tasks/src/delegate/launch.ts +0 -484
- package/packages/background-tasks/src/delegate/result-package.ts +0 -459
- package/packages/background-tasks/src/delegate/runner.ts +0 -430
- package/packages/background-tasks/src/delegate/seed.ts +0 -423
- package/packages/background-tasks/src/delegate/types.ts +0 -323
- package/packages/background-tasks/src/delegate-child-extension.ts +0 -978
- package/packages/background-tasks/src/delegate-extension.ts +0 -654
- package/packages/background-tasks/src/fixtures/delegate-context-incident.json +0 -17
- package/packages/compactor/skills/compactor-detail/SKILL.md +0 -116
- package/packages/compactor/skills/compactor-doctor/SKILL.md +0 -71
- package/packages/compactor/skills/compactor-stats/SKILL.md +0 -51
- package/packages/compactor/src/compaction/build-sections.ts +0 -76
- package/packages/compactor/src/compaction/extract/goals.ts +0 -36
- package/packages/compactor/src/compaction/extract/preferences.ts +0 -40
- package/packages/compactor/src/compaction/format.ts +0 -53
- package/packages/compactor/src/compaction/merge.ts +0 -143
- package/packages/compactor/src/config/presets.ts +0 -160
- package/packages/compactor/src/executor/executor.ts +0 -233
- package/packages/compactor/src/executor/runtime.ts +0 -115
- package/packages/compactor/src/security/evaluator.ts +0 -89
- package/packages/compactor/src/security/policy.ts +0 -96
- package/packages/compactor/src/security/scanner.ts +0 -65
- package/packages/compactor/src/session/auto-inject.ts +0 -60
- package/packages/compactor/src/session/db.ts +0 -325
- package/packages/compactor/src/session/extract.ts +0 -144
- package/packages/compactor/src/session/resume-inject.ts +0 -64
- package/packages/compactor/src/session/snapshot.ts +0 -327
- package/packages/compactor/src/tools/ctx-batch-execute.ts +0 -41
- package/packages/compactor/src/tools/ctx-execute-file.ts +0 -23
- package/packages/compactor/src/tools/ctx-execute.ts +0 -23
- package/packages/compactor/src/tools/ctx-stats.ts +0 -96
- package/packages/image/README.md +0 -99
- package/packages/image/package.json +0 -61
- package/packages/image/skills/image/SKILL.md +0 -76
- package/packages/image/src/generate.ts +0 -256
- package/packages/image/src/index.ts +0 -135
- package/packages/image/src/models.ts +0 -519
- package/packages/image/src/openai-images-api.ts +0 -282
- package/packages/image/src/register-providers.ts +0 -209
- package/packages/image/src/settings.ts +0 -173
- package/packages/image/src/tools.ts +0 -326
- package/packages/input-shortcuts/src/clipboard.ts +0 -119
- package/packages/kanboard/src/runner.ts +0 -817
- package/packages/skill-registry/skills/auto/SKILL.md +0 -465
- package/packages/skill-registry/skills/brainstorm/SKILL.md +0 -219
- package/packages/skill-registry/skills/chore-create/SKILL.md +0 -315
- package/packages/skill-registry/skills/chore-execute/SKILL.md +0 -314
- package/packages/skill-registry/skills/consolidate/SKILL.md +0 -142
- package/packages/skill-registry/skills/consultant/SKILL.md +0 -97
- package/packages/skill-registry/skills/debug/SKILL.md +0 -228
- package/packages/skill-registry/skills/document/SKILL.md +0 -120
- package/packages/skill-registry/skills/fix/SKILL.md +0 -212
- package/packages/skill-registry/skills/gather-context/SKILL.md +0 -145
- package/packages/skill-registry/skills/plan/SKILL.md +0 -231
- package/packages/skill-registry/skills/quick-fix/SKILL.md +0 -135
- package/packages/skill-registry/skills/quick-work/SKILL.md +0 -110
- package/packages/skill-registry/skills/research/SKILL.md +0 -251
- package/packages/skill-registry/skills/review-work/SKILL.md +0 -187
- package/packages/skill-registry/skills/scan-issues/SKILL.md +0 -195
- package/packages/skill-registry/skills/work/SKILL.md +0 -218
- package/packages/skill-registry/skills/worktree-create/SKILL.md +0 -69
- package/packages/skill-registry/skills/worktree-list/SKILL.md +0 -78
- package/packages/skill-registry/skills/worktree-merge/SKILL.md +0 -89
- package/packages/subagents/agents/delegate.md +0 -14
- package/packages/subagents/agents/oracle.md +0 -78
- package/packages/subagents/agents/researcher.md +0 -52
- package/packages/subagents/agents/reviewer.md +0 -80
- package/packages/subagents/agents/scout.md +0 -50
- package/packages/subagents/agents/worker.md +0 -59
- package/packages/subagents/prompts/council.md +0 -48
- package/packages/subagents/prompts/gather-context-and-clarify.md +0 -13
- package/packages/subagents/prompts/parallel-cleanup.md +0 -59
- package/packages/subagents/prompts/parallel-research.md +0 -50
- package/packages/subagents/prompts/parallel-review.md +0 -54
- package/packages/subagents/prompts/review-loop.md +0 -43
- package/packages/subagents/skills/council-mode/SKILL.md +0 -230
- package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +0 -259
- package/packages/subagents/skills/subagents/references/execution-controls.md +0 -454
- package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +0 -161
- package/packages/subagents/skills/subagents/references/multi-lane-orchestration.md +0 -39
- package/packages/subagents/skills/subagents/references/prompting-and-roles.md +0 -267
- package/packages/subagents/src/acceptance.ts +0 -331
- package/packages/subagents/src/agent-manager.ts +0 -405
- package/packages/subagents/src/agent-memory.ts +0 -300
- package/packages/subagents/src/agent-overrides.ts +0 -183
- package/packages/subagents/src/agent-runner.ts +0 -271
- package/packages/subagents/src/async-runner.ts +0 -422
- package/packages/subagents/src/authority-policy.ts +0 -47
- package/packages/subagents/src/budgets.ts +0 -285
- package/packages/subagents/src/child-safety.ts +0 -140
- package/packages/subagents/src/config.ts +0 -249
- package/packages/subagents/src/conversation-viewer.ts +0 -281
- package/packages/subagents/src/core-compat.ts +0 -94
- package/packages/subagents/src/custom-agents.ts +0 -343
- package/packages/subagents/src/file-system-retry.ts +0 -50
- package/packages/subagents/src/fleet-data.ts +0 -105
- package/packages/subagents/src/fleet-view.ts +0 -322
- package/packages/subagents/src/foreground-detach.ts +0 -59
- package/packages/subagents/src/fork-context.ts +0 -285
- package/packages/subagents/src/global.d.ts +0 -7
- package/packages/subagents/src/guide.ts +0 -129
- package/packages/subagents/src/mission-state.ts +0 -133
- package/packages/subagents/src/mission-store.ts +0 -436
- package/packages/subagents/src/output-limits.ts +0 -142
- package/packages/subagents/src/parity-types.ts +0 -315
- package/packages/subagents/src/pi-args.ts +0 -167
- package/packages/subagents/src/result-files.ts +0 -232
- package/packages/subagents/src/result-watcher.ts +0 -221
- package/packages/subagents/src/retained-children.ts +0 -176
- package/packages/subagents/src/run-fanout-budget.ts +0 -356
- package/packages/subagents/src/scheduled-runs.ts +0 -349
- package/packages/subagents/src/schemas.ts +0 -263
- package/packages/subagents/src/slash-commands.ts +0 -80
- package/packages/subagents/src/supervisor-channel.ts +0 -249
- package/packages/subagents/src/tool-handler.ts +0 -1144
- package/packages/subagents/src/types.ts +0 -238
- package/packages/subagents/src/widget.ts +0 -457
- package/packages/subagents/src/workflow-script.ts +0 -737
- package/packages/subagents/src/workflow-worker.ts +0 -384
- package/packages/subagents/src/worktree.ts +0 -614
- package/packages/utility/src/analytics/collector.ts +0 -293
- package/packages/utility/src/info-screen.ts +0 -80
- package/packages/utility/src/lifecycle/process.ts +0 -162
- package/packages/utility/src/prefix-cache.ts +0 -263
- package/packages/utility/src/skill-discovery.ts +0 -542
- package/packages/utility/src/tools/env.ts +0 -113
- package/packages/utility/src/tui/name-badge-state.ts +0 -315
- package/packages/utility/src/tui/name-badge.ts +0 -109
- /package/packages/{subagents/src → core/src/child-agent}/pi-spawn.ts +0 -0
|
@@ -1,60 +1,594 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Lossless (zero-LLM) summary — state first, hard-budgeted, rebuilt from the
|
|
3
|
+
* full session history every time.
|
|
4
|
+
*
|
|
5
|
+
* Section order is what a model resuming the work needs first:
|
|
6
|
+
* Active Work → Your Requests → Latest State → Decisions & Constraints →
|
|
7
|
+
* Files → Commits → Open Errors → Recent Transcript
|
|
8
|
+
* Every section has its own share of the budget and every item is clipped, so
|
|
9
|
+
* no single message (a pasted log, an injected snapshot) can blow the summary up.
|
|
3
10
|
*/
|
|
4
11
|
|
|
5
|
-
import
|
|
6
|
-
import type {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { mergePrevious, mergeBriefTranscriptWithFreshBudget } from "./merge.js";
|
|
12
|
-
import { selectRankedBriefBlocks, type BriefRankingOptions } from "./rank.js";
|
|
12
|
+
import { relative, isAbsolute } from "node:path";
|
|
13
|
+
import type { FileOps, NormalizedBlock, SummarySections } from "../types.js";
|
|
14
|
+
import type { SummarySource } from "./source.js";
|
|
15
|
+
import { extractPath } from "./extract/files.js";
|
|
16
|
+
import { selectRankedBriefBlocks } from "./rank.js";
|
|
17
|
+
import { compileBrief } from "./brief.js";
|
|
13
18
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const fresh = formatSummary(data, { capBriefTranscript: options.capFreshBrief ?? true });
|
|
30
|
-
const prev = input.previousSummary
|
|
31
|
-
? stripRecallNote(input.previousSummary)
|
|
32
|
-
: undefined;
|
|
33
|
-
const merged = prev
|
|
34
|
-
? mergePrevious(prev, fresh, { preserveFreshBrief: options.preserveFreshBriefOnMerge })
|
|
35
|
-
: fresh;
|
|
36
|
-
if (!merged) return "";
|
|
37
|
-
return merged + "\n\n---\n\n" + RECALL_NOTE;
|
|
19
|
+
export const RECALL_NOTE =
|
|
20
|
+
"Everything above is condensed from the full session history, which is kept. " +
|
|
21
|
+
"Use `session_recall` to search it (refs like #123 are entry indices; pass them in `expand` for full content). " +
|
|
22
|
+
"Do not redo work that is already done.";
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_SECTIONS: SummarySections = {
|
|
25
|
+
activeWork: true,
|
|
26
|
+
requests: true,
|
|
27
|
+
state: true,
|
|
28
|
+
decisions: true,
|
|
29
|
+
files: true,
|
|
30
|
+
commits: true,
|
|
31
|
+
errors: true,
|
|
32
|
+
lessons: true,
|
|
33
|
+
transcript: true,
|
|
38
34
|
};
|
|
39
35
|
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
36
|
+
export interface LosslessSummaryInput {
|
|
37
|
+
source: SummarySource;
|
|
38
|
+
/** Authoritative active-work blocks from modules (goal, ralph, kanboard…). */
|
|
39
|
+
activeWork?: ReadonlyArray<{ id: string; text: string }>;
|
|
40
|
+
/** Target size in characters (tokens × chars/token). */
|
|
41
|
+
budgetChars: number;
|
|
42
|
+
cwd?: string;
|
|
43
|
+
fileOps?: FileOps;
|
|
44
|
+
sections?: SummarySections;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LosslessSummary {
|
|
48
|
+
text: string;
|
|
49
|
+
sections: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Auto budget in tokens: scales with the amount of history, 1.5k–4k. */
|
|
53
|
+
export function autoBudgetTokens(blockCount: number): number {
|
|
54
|
+
return Math.round(Math.min(4000, Math.max(1500, 1500 + blockCount * 8)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── text helpers ─────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
const oneLine = (text: string): string => text.replace(/\s+/g, " ").trim();
|
|
60
|
+
|
|
61
|
+
/** Stable identity of a summary item (survives clipping to ≥80 chars). */
|
|
62
|
+
export const itemKey = (text: string): string => oneLine(text).slice(0, 80).toLowerCase();
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
export function clip(text: string, max: number): string {
|
|
66
|
+
if (max <= 1) return "";
|
|
67
|
+
return text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Add items while they fit in maxChars (each counted with its "- " line). */
|
|
71
|
+
function fitItems(items: string[], maxChars: number): string[] {
|
|
72
|
+
const out: string[] = [];
|
|
73
|
+
let used = 0;
|
|
74
|
+
for (const item of items) {
|
|
75
|
+
const cost = item.length + 3;
|
|
76
|
+
if (used + cost > maxChars) break;
|
|
77
|
+
out.push(item);
|
|
78
|
+
used += cost;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function section(title: string, lines: string[]): string {
|
|
84
|
+
return lines.length === 0 ? "" : `[${title}]\n${lines.map((l) => `- ${l}`).join("\n")}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Your Requests ────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
const TRIVIAL_REQUEST_RE =
|
|
90
|
+
/^(?:continue|go on|go ahead|carry on|keep going|proceed|next|go|ok(?:ay)?|yes|yep|y|sure|thanks?|thank you|ty|k|done|resume|restarted[^.]*)[\s.!~]*$/i;
|
|
91
|
+
|
|
92
|
+
export interface RequestSelection {
|
|
93
|
+
lines: string[];
|
|
94
|
+
/** Indices (into the input) of requests shown verbatim. */
|
|
95
|
+
shown: Set<number>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function selectRequestLines(requests: string[], maxChars: number): RequestSelection {
|
|
99
|
+
const all = requests
|
|
100
|
+
.map((r, i) => ({ text: oneLine(r), i }))
|
|
101
|
+
.filter((r) => r.text.length > 0 && !TRIVIAL_REQUEST_RE.test(r.text));
|
|
102
|
+
// The opening request and the latest one always stay.
|
|
103
|
+
const meaningful = all;
|
|
104
|
+
const shown = new Set<number>();
|
|
105
|
+
if (meaningful.length === 0) return { lines: [], shown };
|
|
106
|
+
const [first, ...rest] = meaningful;
|
|
107
|
+
const perItem = Math.max(160, Math.floor(maxChars / Math.min(6, meaningful.length)) - 4);
|
|
108
|
+
// The session's opening request: the purpose, kept short.
|
|
109
|
+
const lines = [clip(first.text, 240)];
|
|
110
|
+
shown.add(first.i);
|
|
111
|
+
let budget = maxChars - lines[0].length - 3;
|
|
112
|
+
// Newest first until the budget runs out, then restore chronological order.
|
|
113
|
+
const recent: string[] = [];
|
|
114
|
+
for (let k = rest.length - 1; k >= 0 && recent.length < 5; k--) {
|
|
115
|
+
const item = clip(rest[k].text, perItem);
|
|
116
|
+
if (item.length + 3 > budget) break;
|
|
117
|
+
recent.unshift(item);
|
|
118
|
+
shown.add(rest[k].i);
|
|
119
|
+
budget -= item.length + 3;
|
|
120
|
+
}
|
|
121
|
+
const omitted = rest.length - recent.length;
|
|
122
|
+
if (omitted > 0) lines.push(`(${omitted} more request${omitted === 1 ? "" : "s"} in between — session_recall has them)`);
|
|
123
|
+
return { lines: [...lines, ...recent], shown };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const selectRequests = (requests: string[], maxChars: number): string[] => selectRequestLines(requests, maxChars).lines;
|
|
127
|
+
|
|
128
|
+
// ── Decisions & Constraints ──────────────────────────────
|
|
129
|
+
|
|
130
|
+
/** An instruction opens with an imperative or carries a modal; "X don't work" is a bug report. */
|
|
131
|
+
const IMPERATIVE_START_RE =
|
|
132
|
+
/^(?:(?:please|also|and|then|but|so)[, ]+)?(?:don'?t|do not|never|always|keep|avoid|make sure|ensure|use|prefer|remember|only use|stop|no need|restore|revert|undo|preserve|stick to|stay with|go back to)\b/i;
|
|
133
|
+
const MODAL_RE = /\b(?:should(?:n'?t)?|must|prefer|instead of|instead|rather than|required|important|no need)\b/i;
|
|
134
|
+
/** The user rejecting something the agent did: a correction that must stick. */
|
|
135
|
+
const CORRECTION_RE = /\bI (?:did not|didn't|never) (?:ask|request|want|say)\b|\bnot what I (?:asked|wanted|meant)\b|\bwhy (?:did|do) you (?:change|remove|delete)\b/i;
|
|
136
|
+
const DECISION_RE = { test: (piece: string) => IMPERATIVE_START_RE.test(piece) || MODAL_RE.test(piece) || CORRECTION_RE.test(piece) };
|
|
137
|
+
|
|
138
|
+
const PASTED_AI_RE = /\bI(?:'m| am) an AI\b|\bas an AI\b|\bI can help (?:with|you)\b|\bif you'd like, I\b|\bI can'?t do that\b|\*\*[^*\n]+\*\*/i;
|
|
139
|
+
|
|
140
|
+
/** Text the user quoted or pasted (code fences, > quotes) is material, not instructions. */
|
|
141
|
+
const stripQuoted = (text: string): string =>
|
|
142
|
+
text.replace(/```[\s\S]*?(?:```|$)/g, "\n").replace(/^\s*>.*$/gm, "");
|
|
143
|
+
|
|
144
|
+
export function selectDecisions(
|
|
145
|
+
requests: string[],
|
|
146
|
+
maxChars: number,
|
|
147
|
+
skip: ReadonlySet<number> = new Set(),
|
|
148
|
+
): string[] {
|
|
149
|
+
const seen = new Set<string>();
|
|
150
|
+
const found: string[] = [];
|
|
151
|
+
for (const [idx, request] of requests.entries()) {
|
|
152
|
+
if (skip.has(idx) || TRIVIAL_REQUEST_RE.test(oneLine(request))) continue;
|
|
153
|
+
// An answer to an agent question is one explicit decision: keep it whole.
|
|
154
|
+
if (request.includes(" → ")) {
|
|
155
|
+
const key = request.toLowerCase().slice(0, 60);
|
|
156
|
+
if (!seen.has(key)) {
|
|
157
|
+
seen.add(key);
|
|
158
|
+
found.push(clip(oneLine(request), 260));
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const pieces = stripQuoted(request)
|
|
163
|
+
.split(/\n+/)
|
|
164
|
+
.flatMap((line) => line.split(/(?<=[.!?])\s+(?=[A-Z0-9"'`])/))
|
|
165
|
+
.map((p) => p.replace(/^\s*(?:[-*+>]|\d+[.)])\s+/, "").trim())
|
|
166
|
+
.filter((p) => p.length >= 24 && p.length <= 400);
|
|
167
|
+
let pushedFromThis = -1;
|
|
168
|
+
for (const piece of pieces) {
|
|
169
|
+
if (!DECISION_RE.test(piece)) continue;
|
|
170
|
+
// "I did not request this" belongs to the instruction right before it.
|
|
171
|
+
if (pushedFromThis >= 0 && CORRECTION_RE.test(piece) && !IMPERATIVE_START_RE.test(piece) && !MODAL_RE.test(piece)) {
|
|
172
|
+
found[pushedFromThis] = clip(`${found[pushedFromThis]} ${oneLine(piece)}`, 260);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (/\?["')\]]*$/.test(piece)) continue; // a question, not a decision
|
|
176
|
+
if (PASTED_AI_RE.test(piece)) continue; // pasted assistant output
|
|
177
|
+
if (/^(?:it|this|that|these|those)\b/i.test(piece) && piece.length < 60) continue; // needs context it lacks
|
|
178
|
+
if (/^[`$]|[{};]\s*$/.test(piece)) continue; // code, not a decision
|
|
179
|
+
const key = piece.toLowerCase().slice(0, 60);
|
|
180
|
+
if (seen.has(key)) continue;
|
|
181
|
+
seen.add(key);
|
|
182
|
+
found.push(clip(oneLine(piece), 220));
|
|
183
|
+
pushedFromThis = found.length - 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return pickDecisions(found, maxChars);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A rule for the whole session, not one task: these outlive recency. */
|
|
190
|
+
const STANDING_RE =
|
|
191
|
+
/\b(?:always|never|every|all (?:the )?\w+|by default|default|from now on|going forward|in general|globally|whenever|each time|unless|any time)\b/i;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Half the budget goes to the most recent decisions (the current work); the
|
|
195
|
+
* rest to standing rules from anywhere in the session, oldest first, so an
|
|
196
|
+
* early "never do X" is not pushed out by a flurry of recent task details.
|
|
197
|
+
* Output stays chronological.
|
|
198
|
+
*/
|
|
199
|
+
function pickDecisions(found: string[], maxChars: number): string[] {
|
|
200
|
+
const chosen = new Set<number>();
|
|
201
|
+
let used = 0;
|
|
202
|
+
const take = (i: number) => {
|
|
203
|
+
const cost = found[i].length + 3;
|
|
204
|
+
if (chosen.has(i) || used + cost > maxChars) return false;
|
|
205
|
+
chosen.add(i);
|
|
206
|
+
used += cost;
|
|
207
|
+
return true;
|
|
208
|
+
};
|
|
209
|
+
for (let i = found.length - 1; i >= 0 && used < maxChars / 2; i--) take(i);
|
|
210
|
+
for (let i = 0; i < found.length; i++) {
|
|
211
|
+
if (STANDING_RE.test(found[i]) || CORRECTION_RE.test(found[i])) take(i);
|
|
212
|
+
}
|
|
213
|
+
for (let i = found.length - 1; i >= 0; i--) take(i);
|
|
214
|
+
return [...chosen].sort((a, b) => a - b).map((i) => found[i]);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Latest State ─────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/** A progress report to the user is long; step narration ("Next I'll…") is short. */
|
|
220
|
+
const REPORT_MIN_CHARS = 400;
|
|
221
|
+
|
|
222
|
+
export function selectState(reports: string[], maxChars: number): string[] {
|
|
223
|
+
const flat = reports.map((r) => oneLine(r.replace(/!\[[^\]]*\]\([^)]*\)/g, ""))).filter(Boolean);
|
|
224
|
+
if (flat.length === 0) return [];
|
|
225
|
+
let reportIdx = -1;
|
|
226
|
+
for (let i = flat.length - 1; i >= 0; i--) {
|
|
227
|
+
if (flat[i].length >= REPORT_MIN_CHARS) {
|
|
228
|
+
reportIdx = i;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const latestIdx = flat.length - 1;
|
|
233
|
+
if (reportIdx === latestIdx) return [`Last report: ${clip(flat[reportIdx], maxChars - 20)}`];
|
|
234
|
+
if (reportIdx < 0) return [`Latest step: ${clip(flat[latestIdx], Math.floor(maxChars * 0.3))}`];
|
|
235
|
+
const step = `Latest step: ${clip(flat[latestIdx], Math.floor(maxChars * 0.3))}`;
|
|
236
|
+
const age = latestIdx - reportIdx;
|
|
237
|
+
const label = `Last full report (${age} message${age === 1 ? "" : "s"} earlier — may be outdated)`;
|
|
238
|
+
return [step, `${label}: ${clip(flat[reportIdx], maxChars - step.length - label.length - 10)}`];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Files ────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
const EDIT_TOOLS = /^(?:edit|multiedit|apply_patch|quick_edit|target_edit)$/i;
|
|
244
|
+
const WRITE_TOOLS = /^(?:write|create)$/i;
|
|
245
|
+
const READ_TOOLS = /^(?:read)$/i;
|
|
246
|
+
/** Screenshots and scratch files: looked at, not worked on. */
|
|
247
|
+
const NOISE_READ_RE = /(?:^\/tmp\/|\.(?:png|jpe?g|gif|webp|bmp|svg|ico)$)/i;
|
|
248
|
+
|
|
249
|
+
export function selectFiles(blocks: NormalizedBlock[], cwd: string | undefined, maxChars: number): string[] {
|
|
250
|
+
const modified = new Map<string, number>();
|
|
251
|
+
const created = new Set<string>();
|
|
252
|
+
const read = new Set<string>();
|
|
253
|
+
const rel = (p: string) => {
|
|
254
|
+
if (!cwd || !isAbsolute(p)) return p;
|
|
255
|
+
const r = relative(cwd, p);
|
|
256
|
+
return r && !r.startsWith("..") ? r : p;
|
|
257
|
+
};
|
|
258
|
+
for (const b of blocks) {
|
|
259
|
+
if (b.kind !== "tool_call") continue;
|
|
260
|
+
const raw = extractPath(b.args);
|
|
261
|
+
if (!raw) continue;
|
|
262
|
+
const path = rel(raw);
|
|
263
|
+
if (EDIT_TOOLS.test(b.name)) modified.set(path, (modified.get(path) ?? 0) + 1);
|
|
264
|
+
else if (WRITE_TOOLS.test(b.name)) created.add(path);
|
|
265
|
+
else if (READ_TOOLS.test(b.name) && !NOISE_READ_RE.test(path)) read.add(path);
|
|
266
|
+
}
|
|
267
|
+
for (const p of created) if (modified.has(p)) created.delete(p);
|
|
268
|
+
for (const p of [...read]) if (modified.has(p) || created.has(p)) read.delete(p);
|
|
269
|
+
|
|
270
|
+
const list = (label: string, items: string[], share: number) => {
|
|
271
|
+
if (items.length === 0) return "";
|
|
272
|
+
const fitted = fitItems(items, Math.floor(maxChars * share));
|
|
273
|
+
const more = items.length - fitted.length;
|
|
274
|
+
return `${label}: ${fitted.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`;
|
|
275
|
+
};
|
|
276
|
+
const byCount = [...modified.entries()].sort((a, b) => b[1] - a[1]).map(([p, n]) => (n > 1 ? `${p} ×${n}` : p));
|
|
277
|
+
return [
|
|
278
|
+
list("Modified", byCount, 0.45),
|
|
279
|
+
list("Created", [...created], 0.3),
|
|
280
|
+
list("Read", [...read].reverse(), 0.2),
|
|
281
|
+
].filter(Boolean);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Lessons ─────────────────────────────────────────────
|
|
285
|
+
// Failures turn into knowledge only if someone writes the lesson down. The
|
|
286
|
+
// agent usually does, in its own words: memory_store notes, "# Confirmed: …"
|
|
287
|
+
// comments in the commands it runs, and diagnosis sentences ("root cause…",
|
|
288
|
+
// "…silently ignores…"). Keep those verbatim instead of generating anything.
|
|
289
|
+
|
|
290
|
+
const MEMORY_TOOL_RE = /^(?:memory_store|memory_save|memory_add|save_memory|remember)$/i;
|
|
291
|
+
/** Marks a learned fact, not a status update. */
|
|
292
|
+
const LESSON_MARK_RE =
|
|
293
|
+
/\b(?:root cause|turns out|found it|confirmed|gotcha|caveat|silently|no-?ops?|doesn'?t (?:support|work|accept)|not supported|only works|rejects?|ignores?|the fix (?:is|was)|workaround|breaks? (?:when|if)|must (?:use|be|run)|requires?|instead of|never|always)\b/i;
|
|
294
|
+
/** Diagnosis sentences need a stronger mark than comments (prose is chattier). */
|
|
295
|
+
const DIAGNOSIS_RE =
|
|
296
|
+
/\b(?:root cause|turns out|gotcha|caveat|silently|no-?ops?|doesn'?t (?:support|accept)|not supported|only works|rejects?|ignores?|the (?:real )?(?:issue|problem|bug|cause) (?:is|was)|the fix (?:is|was)|workaround)\b/i;
|
|
297
|
+
|
|
298
|
+
/** Debug narration ("my DBG print never fired") is the hunt, not the finding. */
|
|
299
|
+
const DEBUG_RE = /\b(?:DBG|debug(?:ging)? (?:print|log|output)|print(?:ed)? (?:never|didn'?t)|never (?:fired|printed)|let me|I'?ll (?:check|look|try))\b/i;
|
|
300
|
+
|
|
301
|
+
export interface LessonCandidate {
|
|
302
|
+
text: string;
|
|
303
|
+
source: "memory" | "comment" | "diagnosis";
|
|
304
|
+
/** Block index (session order). */
|
|
305
|
+
at: number;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Agent-written lessons in session order (later duplicates win). */
|
|
309
|
+
export function lessonCandidates(blocks: NormalizedBlock[]): LessonCandidate[] {
|
|
310
|
+
const out = new Map<string, LessonCandidate>();
|
|
311
|
+
let at = 0;
|
|
312
|
+
const add = (text: string, source: LessonCandidate["source"]) => {
|
|
313
|
+
const t = oneLine(text);
|
|
314
|
+
if (t.length < 30) return;
|
|
315
|
+
const key = itemKey(t);
|
|
316
|
+
out.delete(key);
|
|
317
|
+
out.set(key, { text: clip(t, 320), source, at });
|
|
318
|
+
};
|
|
319
|
+
for (const [i, b] of blocks.entries()) {
|
|
320
|
+
at = i;
|
|
321
|
+
if (b.kind === "tool_call" && MEMORY_TOOL_RE.test(b.name)) {
|
|
322
|
+
const raw = [b.args.content, b.args.text, b.args.memory, b.args.note].find((v) => typeof v === "string") as string | undefined;
|
|
323
|
+
// Past a bare header ("ROOT CAUSES CONFIRMED:"): lines until there is substance.
|
|
324
|
+
if (raw) {
|
|
325
|
+
const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
326
|
+
let text = "";
|
|
327
|
+
for (const l of lines) {
|
|
328
|
+
text = text ? `${text} ${l}` : l;
|
|
329
|
+
if (text.length >= 160) break;
|
|
330
|
+
}
|
|
331
|
+
add(text, "memory");
|
|
332
|
+
}
|
|
333
|
+
} else if (b.kind === "tool_call" && typeof b.args.command === "string") {
|
|
334
|
+
for (const line of b.args.command.split("\n")) {
|
|
335
|
+
const m = line.match(/^\s*#+\s*(.+)$/);
|
|
336
|
+
// A comment ending in ":" introduces the next command; it is not a finding.
|
|
337
|
+
if (m && m[1].length <= 300 && !/:\s*$/.test(m[1]) && !DEBUG_RE.test(m[1]) && (DIAGNOSIS_RE.test(m[1]) || /\b(?:confirmed|found it)\b/i.test(m[1])) && LESSON_MARK_RE.test(m[1])) add(m[1], "comment");
|
|
338
|
+
}
|
|
339
|
+
} else if (b.kind === "assistant") {
|
|
340
|
+
for (const sentence of b.text.replace(/```[\s\S]*?```/g, " ").split(/(?<=[.!?])\s+|\n+/)) {
|
|
341
|
+
const t = sentence.trim();
|
|
342
|
+
if (t.length >= 50 && t.length <= 320 && !t.endsWith("?") && !DEBUG_RE.test(t) && DIAGNOSIS_RE.test(t)) add(t, "diagnosis");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return [...out.values()];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const LESSON_PRIORITY: Record<LessonCandidate["source"], number> = { memory: 0, comment: 1, diagnosis: 2 };
|
|
350
|
+
|
|
351
|
+
/** Memory notes first, then comments, then diagnoses; newest first within each; chronological output. */
|
|
352
|
+
export function selectLessons(blocks: NormalizedBlock[], maxChars: number): string[] {
|
|
353
|
+
const all = lessonCandidates(blocks).map((c, i) => ({ ...c, i }));
|
|
354
|
+
const ranked = [...all].sort((a, b) => LESSON_PRIORITY[a.source] - LESSON_PRIORITY[b.source] || b.i - a.i);
|
|
355
|
+
const chosen: typeof all = [];
|
|
356
|
+
let used = 0;
|
|
357
|
+
for (const c of ranked) {
|
|
358
|
+
if (used + c.text.length + 3 > maxChars) continue;
|
|
359
|
+
chosen.push(c);
|
|
360
|
+
used += c.text.length + 3;
|
|
361
|
+
}
|
|
362
|
+
return chosen.sort((a, b) => a.i - b.i).map((c) => c.text);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── Project Knowledge ───────────────────────────────────
|
|
366
|
+
// What the agent learned about the project that no user message states:
|
|
367
|
+
// the notes it wrote (where deploy steps and conventions live), the commands
|
|
368
|
+
// it ran repeatedly, and the hosts it worked against.
|
|
369
|
+
|
|
370
|
+
const NOTE_RE = /(?:^|\/)(?:AGENTS?|CLAUDE|DESIGN|README|SKILL|CONTRIBUTING|RUNBOOK|NOTES)\.md$|(?:^|\/)docs\/.+\.md$|\.agents\/skills\/.+\.md$/i;
|
|
371
|
+
const TOOLING_RE = /^(?:npm|npx|pnpm|yarn|bun|bunx|cargo|mise|make|just|go|pytest|uv|python3? -m|docker|docker-compose|kubectl|helm|rsync|scp|ssh|git push|gh|tea|systemctl|terraform|deno|flutter|xcodebuild)\b/;
|
|
372
|
+
|
|
373
|
+
/** The first meaningful command of a shell line: no `cd x &&`, pipes, redirects or env prefixes. */
|
|
374
|
+
function commandCore(command: string): string {
|
|
375
|
+
const first = command.split("\n").find((l) => l.trim() && !l.trim().startsWith("#")) ?? "";
|
|
376
|
+
const segments = first.split(/\s*(?:&&|;)\s*/).map((x) => x.trim()).filter(Boolean);
|
|
377
|
+
const main = segments.find((x) => !/^(?:cd|export|source|set|echo)\b/.test(x)) ?? "";
|
|
378
|
+
return main
|
|
379
|
+
.replace(/^(?:[A-Z_][A-Z0-9_]*=\S+\s+)+/, "")
|
|
380
|
+
.replace(/\s*(?:\||2>&1|>\s*\S+|>>\s*\S+).*$/, "")
|
|
381
|
+
.trim();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function selectKnowledge(blocks: NormalizedBlock[], cwd: string | undefined, maxChars: number): string[] {
|
|
385
|
+
const rel = (p: string) => {
|
|
386
|
+
if (!cwd || !isAbsolute(p)) return p;
|
|
387
|
+
const r = relative(cwd, p);
|
|
388
|
+
return r && !r.startsWith("..") ? r : p;
|
|
389
|
+
};
|
|
390
|
+
const edited = new Set<string>();
|
|
391
|
+
const read = new Set<string>();
|
|
392
|
+
const commands = new Map<string, { text: string; n: number }>();
|
|
393
|
+
const hosts = new Map<string, number>();
|
|
394
|
+
const addHost = (h: string) => hosts.set(h, (hosts.get(h) ?? 0) + 1);
|
|
395
|
+
for (const b of blocks) {
|
|
396
|
+
if (b.kind !== "tool_call") continue;
|
|
397
|
+
const path = extractPath(b.args);
|
|
398
|
+
if (path && NOTE_RE.test(path)) (EDIT_TOOLS.test(b.name) || WRITE_TOOLS.test(b.name) ? edited : read).add(rel(path));
|
|
399
|
+
const command = typeof b.args.command === "string" ? b.args.command : "";
|
|
400
|
+
if (!command) continue;
|
|
401
|
+
let core = commandCore(command);
|
|
402
|
+
// ssh: the connection is the reusable part, not the remote script.
|
|
403
|
+
if (/^ssh\b/.test(core)) core = core.split(/\s['"]/)[0].trim();
|
|
404
|
+
if (TOOLING_RE.test(core) && core.length <= 160 && !/<<|\\$/.test(core) && (core.match(/['"]/g)?.length ?? 0) % 2 === 0) {
|
|
405
|
+
const key = core.split(/\s+/).slice(0, 3).join(" ");
|
|
406
|
+
const prev = commands.get(key);
|
|
407
|
+
commands.set(key, { text: core, n: (prev?.n ?? 0) + 1 });
|
|
408
|
+
}
|
|
409
|
+
for (const m of command.matchAll(/https?:\/\/([\w.-]+(?::\d+)?)/g)) addHost(m[1]);
|
|
410
|
+
for (const m of command.matchAll(/\b(?:ssh|scp|rsync)\b[^\n]*?\b([\w.-]+@[\w.-]+)/g)) addHost(m[1]);
|
|
411
|
+
}
|
|
412
|
+
for (const p of edited) read.delete(p);
|
|
413
|
+
const lines: string[] = [];
|
|
414
|
+
const cap = (label: string, items: string[], share: number) => {
|
|
415
|
+
if (items.length === 0) return;
|
|
416
|
+
const fitted = fitItems(items, Math.floor(maxChars * share));
|
|
417
|
+
const more = items.length - fitted.length;
|
|
418
|
+
if (fitted.length) lines.push(`${label}: ${fitted.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`);
|
|
419
|
+
};
|
|
420
|
+
cap("Notes written", [...edited], 0.2);
|
|
421
|
+
cap("Notes read", [...read].slice(-8), 0.15);
|
|
422
|
+
const repeated = [...commands.values()].filter((c) => c.n >= 2).sort((a, b) => b.n - a.n).map((c) => `\`${c.text}\` ×${c.n}`);
|
|
423
|
+
cap("Repeated commands", repeated.slice(0, 8), 0.45);
|
|
424
|
+
const topHosts = [...hosts.entries()].filter(([, n]) => n >= 2).sort((a, b) => b[1] - a[1]).map(([h]) => h);
|
|
425
|
+
cap("Hosts", topHosts.slice(0, 6), 0.2);
|
|
426
|
+
return lines;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ── Commits ──────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
const COMMIT_LINE_RE = /\[([\w./@-]+)(?: \(root-commit\))? ([0-9a-f]{7,40})\] (.+?)(?= \d+ files? changed|$)/g;
|
|
432
|
+
/** `git commit -m "msg"` / `-qm 'msg'` — for quiet commits that print no summary line. */
|
|
433
|
+
const COMMIT_CMD_RE = /\bgit\s+commit\b[^"'\n]*?\s-[a-zA-Z]*m\s*(["'])((?:(?!\1).){3,}?)\1/;
|
|
434
|
+
|
|
435
|
+
export function selectCommits(blocks: NormalizedBlock[], maxChars: number): string[] {
|
|
436
|
+
const commits = new Map<string, string>();
|
|
437
|
+
let pendingSubject: string | null = null;
|
|
438
|
+
for (const b of blocks) {
|
|
439
|
+
if (b.kind === "tool_call") {
|
|
440
|
+
const command = typeof b.args.command === "string" ? b.args.command : "";
|
|
441
|
+
pendingSubject = COMMIT_CMD_RE.exec(command)?.[2] ?? null;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (b.kind !== "tool_result") continue;
|
|
445
|
+
let found = false;
|
|
446
|
+
if (!b.isError) {
|
|
447
|
+
for (const m of b.text.matchAll(COMMIT_LINE_RE)) {
|
|
448
|
+
commits.set(m[2].slice(0, 8), clip(m[3].replace(/\s+(?:Date|Author|Co-authored-by):.*$/i, "").trim(), 110));
|
|
449
|
+
found = true;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (!found && pendingSubject && !b.isError) commits.set(`~${commits.size}`, clip(pendingSubject.trim(), 110));
|
|
453
|
+
pendingSubject = null;
|
|
454
|
+
}
|
|
455
|
+
const lines = [...commits.entries()].map(([hash, subject]) => (hash.startsWith("~") ? subject : `${hash} ${subject}`));
|
|
456
|
+
return fitItems(lines.slice(-10).reverse(), maxChars).reverse();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Open Errors ──────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
const callTarget = (b: Extract<NormalizedBlock, { kind: "tool_call" }>): string => {
|
|
462
|
+
const path = extractPath(b.args);
|
|
463
|
+
if (path) return path;
|
|
464
|
+
const command = typeof b.args.command === "string" ? b.args.command : "";
|
|
465
|
+
return oneLine(command).slice(0, 60);
|
|
58
466
|
};
|
|
59
467
|
|
|
60
|
-
|
|
468
|
+
/** Later successes of the same tool after which an error counts as moved past. */
|
|
469
|
+
const RESOLVED_AFTER_SUCCESSES = 3;
|
|
470
|
+
|
|
471
|
+
export function selectOpenErrors(blocks: NormalizedBlock[], maxChars: number): string[] {
|
|
472
|
+
const lastTarget = new Map<string, string>();
|
|
473
|
+
const open = new Map<string, { tool: string; text: string; ref?: number; successesAfter: number }>();
|
|
474
|
+
// Only recent errors can still be open; older ones are history (session_recall).
|
|
475
|
+
for (const b of blocks.slice(-TRANSCRIPT_WINDOW_BLOCKS)) {
|
|
476
|
+
if (b.kind === "tool_call") {
|
|
477
|
+
lastTarget.set(b.name, callTarget(b));
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (b.kind !== "tool_result") continue;
|
|
481
|
+
const key = `${b.name}\u0000${lastTarget.get(b.name) ?? ""}`;
|
|
482
|
+
open.delete(key);
|
|
483
|
+
if (b.isError) {
|
|
484
|
+
open.set(key, { tool: b.name, text: `[${b.name}] ${clip(oneLine(b.text), 200)}`, ref: b.sourceIndex, successesAfter: 0 });
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
// A retry with the same target clears its error (above); other successes
|
|
488
|
+
// of the same tool mean the work moved on.
|
|
489
|
+
for (const [k, err] of open) {
|
|
490
|
+
if (err.tool === b.name && ++err.successesAfter >= RESOLVED_AFTER_SUCCESSES) open.delete(k);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const lines = [...open.values()].map((e) => (e.ref != null ? `${e.text} (#${e.ref})` : e.text));
|
|
494
|
+
return fitItems(lines.slice(-5).reverse(), maxChars).reverse();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ── Recent Transcript ────────────────────────────────────
|
|
498
|
+
|
|
499
|
+
/** The transcript covers recent work; older history lives in the sections above. */
|
|
500
|
+
export const TRANSCRIPT_WINDOW_BLOCKS = 160;
|
|
501
|
+
|
|
502
|
+
export function selectTranscript(blocks: NormalizedBlock[], maxChars: number, fileOps?: FileOps): string {
|
|
503
|
+
if (maxChars < 200 || blocks.length === 0) return "";
|
|
504
|
+
const recent = blocks.slice(-TRANSCRIPT_WINDOW_BLOCKS);
|
|
505
|
+
const selected = selectRankedBriefBlocks(recent, { maxBriefChars: maxChars, fileOps });
|
|
506
|
+
const lines = compileBrief(selected).split("\n").map((l) => clip(l, 300));
|
|
507
|
+
// Hard cap: keep the newest lines that fit.
|
|
508
|
+
const out: string[] = [];
|
|
509
|
+
let used = 0;
|
|
510
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
511
|
+
if (used + lines[i].length + 1 > maxChars) break;
|
|
512
|
+
out.unshift(lines[i]);
|
|
513
|
+
used += lines[i].length + 1;
|
|
514
|
+
}
|
|
515
|
+
while (out.length > 0 && !out[0].startsWith("[")) out.shift();
|
|
516
|
+
return out.join("\n").trim();
|
|
517
|
+
}
|
|
518
|
+
// ── secrets ──────────────────────────────────────────────
|
|
519
|
+
|
|
520
|
+
const SECRET_PATTERNS: ReadonlyArray<[RegExp, string]> = [
|
|
521
|
+
[/\b(password|passwd|passphrase|pwd|token|secret|api[_ -]?key)(\s*(?:is|=|:)\s*)(['"`]?)([^\s'"`]{4,})\3/gi, "$1$2[redacted]"],
|
|
522
|
+
// "use password 'x'", "token `x`": a quoted value right after the word.
|
|
523
|
+
[/\b(password|passwd|passphrase|pwd|token|secret|api[_ -]?key)(\s+)(['"`])([^'"`\n]{4,}?)\3/gi, "$1$2[redacted]"],
|
|
524
|
+
[/\bsk-[A-Za-z0-9_-]{16,}/g, "[redacted]"],
|
|
525
|
+
[/\b(Bearer|X-API-KEY:?|Authorization:)\s+[A-Za-z0-9._~+\/=-]{12,}/gi, "$1 [redacted]"],
|
|
526
|
+
[/\bgh[pousr]_[A-Za-z0-9]{20,}/g, "[redacted]"],
|
|
527
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, "[redacted]"],
|
|
528
|
+
[/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, "[redacted]"],
|
|
529
|
+
[/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, "[redacted]"],
|
|
530
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, "[redacted private key]"],
|
|
531
|
+
];
|
|
532
|
+
|
|
533
|
+
/** Summaries are re-sent every turn: keep credentials out (session_recall still has them). */
|
|
534
|
+
export function redactSecrets(text: string): string {
|
|
535
|
+
return SECRET_PATTERNS.reduce((t, [re, to]) => t.replace(re, to), text);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ── assembly ─────────────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
const SHARES = {
|
|
541
|
+
activeWork: 0.16,
|
|
542
|
+
requests: 0.14,
|
|
543
|
+
state: 0.12,
|
|
544
|
+
lessons: 0.1,
|
|
545
|
+
decisions: 0.16,
|
|
546
|
+
files: 0.07,
|
|
547
|
+
knowledge: 0.07,
|
|
548
|
+
commits: 0.05,
|
|
549
|
+
errors: 0.06,
|
|
550
|
+
} as const;
|
|
551
|
+
|
|
552
|
+
export function buildLosslessSummary(input: LosslessSummaryInput): LosslessSummary {
|
|
553
|
+
// Redact before any clipping can cut a secret's closing quote.
|
|
554
|
+
const source: SummarySource = {
|
|
555
|
+
...input.source,
|
|
556
|
+
requests: input.source.requests.map(redactSecrets),
|
|
557
|
+
reports: input.source.reports.map(redactSecrets),
|
|
558
|
+
blocks: input.source.blocks.map((b) => ("text" in b && typeof b.text === "string" ? { ...b, text: redactSecrets(b.text) } : b)),
|
|
559
|
+
};
|
|
560
|
+
const on = { ...DEFAULT_SECTIONS, ...input.sections };
|
|
561
|
+
const budget = Math.max(2000, Math.round(input.budgetChars));
|
|
562
|
+
const cap = (share: number) => Math.floor(budget * share);
|
|
563
|
+
const parts: Array<[string, string]> = [];
|
|
564
|
+
|
|
565
|
+
if (on.activeWork && input.activeWork?.length) {
|
|
566
|
+
const per = Math.floor(cap(SHARES.activeWork) / input.activeWork.length);
|
|
567
|
+
const lines = input.activeWork.map((b) => clip(b.text.trim(), Math.max(200, per)));
|
|
568
|
+
parts.push(["Active Work", `[Active Work]\n${lines.join("\n\n")}`]);
|
|
569
|
+
}
|
|
570
|
+
const requestSel = on.requests ? selectRequestLines(source.requests, cap(SHARES.requests)) : { lines: [], shown: new Set<number>() };
|
|
571
|
+
if (on.requests) parts.push(["Your Requests", section("Your Requests", requestSel.lines)]);
|
|
572
|
+
if (on.state) parts.push(["Latest State", section("Latest State", selectState(source.reports, cap(SHARES.state)))]);
|
|
573
|
+
if (on.decisions) parts.push(["Decisions & Constraints", section("Decisions & Constraints", selectDecisions(source.requests, cap(SHARES.decisions), requestSel.shown))]);
|
|
574
|
+
if (on.files) parts.push(["Files", section("Files", selectFiles(source.blocks, input.cwd, cap(SHARES.files)))]);
|
|
575
|
+
if (on.lessons) parts.push(["Lessons", section("Lessons", selectLessons(source.blocks, cap(SHARES.lessons)))]);
|
|
576
|
+
if (on.files) parts.push(["Project Knowledge", section("Project Knowledge", selectKnowledge(source.blocks, input.cwd, cap(SHARES.knowledge)))]);
|
|
577
|
+
if (on.commits) parts.push(["Commits", section("Commits", selectCommits(source.blocks, cap(SHARES.commits)))]);
|
|
578
|
+
if (on.errors) parts.push(["Open Errors", section("Open Errors", selectOpenErrors(source.blocks, cap(SHARES.errors)))]);
|
|
579
|
+
|
|
580
|
+
const head = parts.filter(([, text]) => text);
|
|
581
|
+
const headText = head.map(([, text]) => text).join("\n\n");
|
|
582
|
+
const sections = head.map(([name]) => name);
|
|
583
|
+
|
|
584
|
+
let transcript = "";
|
|
585
|
+
if (on.transcript) {
|
|
586
|
+
const remaining = budget - headText.length - RECALL_NOTE.length - 40;
|
|
587
|
+
transcript = selectTranscript(source.blocks, Math.max(Math.floor(budget * 0.18), remaining), input.fileOps);
|
|
588
|
+
if (transcript) sections.push("Recent Transcript");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const body = [headText, transcript ? `[Recent Transcript]\n${transcript}` : ""].filter(Boolean).join("\n\n");
|
|
592
|
+
if (!body) return { text: "", sections: [] };
|
|
593
|
+
return { text: `${redactSecrets(body)}\n\n---\n\n${RECALL_NOTE}`, sections };
|
|
594
|
+
}
|