akm-opencode 0.5.1 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +6 -4
  2. package/index.ts +275 -46
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -41,14 +41,14 @@ fails silently when `akm` is not on PATH — the TUI is never affected.
41
41
 
42
42
  | Event | What happens |
43
43
  | --- | --- |
44
- | **`session.created`** (event hook) | Warms the stash index in the background and caches `akm hints` plus active workflow status for the next system transform so the agent knows the CLI surface area at turn 0. |
45
- | **`chat.message`** | Runs `akm curate "<prompt>"` on each user message (prompts shorter than `AKM_CURATE_MIN_CHARS` are skipped). The top matches are stored for injection. Memory intents (prompts mentioning "remember" / "memory") are tracked in the session buffer. |
44
+ | **`session.created`** (event hook) | Warms the stash index in the background, caches `akm hints` plus active workflow status, and runs a scoped `akm curate --run <sessionID>` so fresh sessions see relevant stash context before the first user message. |
45
+ | **`chat.message`** | Runs `akm curate "<prompt>" --run <sessionID>` on each user message (prompts shorter than `AKM_CURATE_MIN_CHARS` are skipped). The top matches are stored for injection. Memory intents (prompts mentioning "remember" / "memory") are tracked in the session buffer. |
46
46
  | **`experimental.chat.system.transform`** | Appends cached hints, active workflow state, the last curator report, and the current prompt's curated context to the model's system prompt. Hints and workflow state are re-injected after transcript compaction. |
47
47
  | **`tool.execute.before`** (`akm_*` tools) | Blocks destructive or sensitive operations until `confirm:true` is provided. |
48
48
  | **`tool.execute.after`** (`akm_*` tools) | Logs asset usage, accumulates refs into the session buffer, records `akm feedback <ref> --positive` / `--negative` asynchronously with per-call dedupe, checkpoints memories every `AKM_MEMORY_CHECKPOINT_EVERY` successful asset-touching tool calls, and scans child-agent free text for additional refs. |
49
49
  | **`experimental.session.compacting`** | Pushes hints, curated context, active workflows, and the last curator report into the compaction prompt so they survive transcript shrinking. |
50
50
  | **`shell.env`** | Exposes `AKM_STASH_DIR`, `AKM_PROJECT`, and `AKM_PLUGIN_VERSION` to shell tools so plain `akm` calls inherit the right context. |
51
- | **`stop`** / **`session.idle`** / **`session.compacted`** / **`session.deleted`** | Flushes the per-session buffer into a `memory:opencode-session-YYYYMMDD-<sid>` memory so every meaningful session contributes durable context for future searches. Requires at least two observations before persisting. |
51
+ | **`stop`** / **`session.idle`** / **`session.compacted`** / **`session.deleted`** | Flushes the per-session buffer into a `memory:opencode-session-YYYYMMDD-<sid>` memory so every meaningful session contributes durable context for future searches. Requires at least two observations before persisting. When `AKM_INDEX_ON_SESSION_END=1`, the hook follows a successful flush with `akm index` so upstream inference/graph passes run immediately. |
52
52
 
53
53
  ### Environment overrides
54
54
 
@@ -58,9 +58,11 @@ fails silently when `akm` is not on PATH — the TUI is never affected.
58
58
  | `AKM_AUTO_FEEDBACK` | `1` | Set to `0` to disable automatic `akm feedback` on tool success/failure. |
59
59
  | `AKM_AUTO_HINTS` | `1` | Set to `0` to skip injecting `akm hints` at session start. |
60
60
  | `AKM_AUTO_MEMORY` | `1` | Set to `0` to disable automatic session-summary memories. |
61
+ | `AKM_INDEX_ON_SESSION_END` | `0` | Set to `1` to run `akm index` after a session-end memory is captured. |
61
62
  | `AKM_CURATE_LIMIT` | `5` | Max curated results injected into context per prompt. |
62
63
  | `AKM_CURATE_MIN_CHARS` | `16` | Minimum prompt length before curation runs. |
63
64
  | `AKM_CURATE_TIMEOUT` | `8` | Wall-clock seconds for `akm` invocations inside hooks. |
65
+ | `AKM_CONTEXT_BUDGET_CHARS` | `4000` | Max total characters injected into system/compaction context for a single turn. |
64
66
  | `AKM_CURATOR_CONTEXT_MAX_CHARS` | `4000` | Max cached curator-report characters re-injected into system/compaction context; the full report is still persisted as memory. |
65
67
  | `AKM_MEMORY_CHECKPOINT_EVERY` | `8` | Number of successful asset-touching tool calls between mid-session checkpoint memories. |
66
68
  | `AKM_RETROSPECTIVE_FEEDBACK_PATTERN` | `\b(thanks|perfect|worked)\b` | Case-insensitive regex used for lightweight positive retrospective feedback on the most recent refs. |
@@ -112,7 +114,7 @@ At least one of `ref` or `query` is required.
112
114
 
113
115
  ## Prerequisites
114
116
 
115
- When the plugin loads, it runs `bun install -g akm-cli@latest` so it always picks up the latest published npm package. It then prefers the Bun-installed binary and falls back to an existing `akm` on PATH when needed. It does not run the standalone shell installers automatically.
117
+ When the plugin loads, it checks the installed `akm` version first and only runs `bun install -g akm-cli@latest` when `akm` is missing or older than the latest stable npm release. Newer pre-releases and local builds are left in place. It then prefers the Bun-installed binary and falls back to an existing `akm` on PATH when needed. It does not run the standalone shell installers automatically.
116
118
 
117
119
  ```sh
118
120
  # macOS / Linux
package/index.ts CHANGED
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url"
7
7
  let resolvedAkmCommand = "akm"
8
8
  const autoInstallPackageRef = "akm-cli@latest"
9
9
  const moduleDir = path.dirname(fileURLToPath(import.meta.url))
10
+ const SEMVER_PATTERN = /\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b/
10
11
 
11
12
  const AKM_AUTO_FEEDBACK = (process.env.AKM_AUTO_FEEDBACK ?? "1") !== "0"
12
13
  const AKM_AUTO_MEMORY = (process.env.AKM_AUTO_MEMORY ?? "1") !== "0"
@@ -32,6 +33,7 @@ const sessionContextEpoch = new Map<string, number>()
32
33
  const sessionContextInjectedEpoch = new Map<string, number>()
33
34
  const sessionCuratedVersion = new Map<string, number>()
34
35
  const sessionCuratedInjectedVersion = new Map<string, number>()
36
+ type ParsedSemver = { core: [number, number, number]; prerelease: Array<string | number> | null }
35
37
  type SessionBufferEntry = {
36
38
  timestamp: string
37
39
  kind: "memory-intent" | "tool-ref"
@@ -205,6 +207,10 @@ function nowIso(): string {
205
207
  return new Date().toISOString()
206
208
  }
207
209
 
210
+ function shouldIndexOnSessionEnd(): boolean {
211
+ return (process.env.AKM_INDEX_ON_SESSION_END ?? "0") === "1"
212
+ }
213
+
208
214
  function buildDateTag(options?: { includeTime?: boolean }): string {
209
215
  const compactIso = new Date().toISOString().replace(/[-:]/g, "")
210
216
  return compactIso.slice(0, options?.includeTime ? CHECKPOINT_DATE_TAG_LENGTH : SESSION_DATE_TAG_LENGTH)
@@ -255,28 +261,57 @@ function runCliSyncRaw(args: string[], timeoutMs: number): { ok: true; stdout: s
255
261
  }
256
262
  }
257
263
 
258
- function runCurateForPrompt(text: string): string | null {
259
- if (!text || text.length < AKM_CURATE_MIN_CHARS) return null
260
- const result = runCliSyncRaw(
261
- [
262
- "--for-agent",
263
- "--format",
264
- "text",
265
- "--detail",
266
- "summary",
267
- "-q",
268
- "curate",
269
- text,
270
- "--limit",
271
- String(AKM_CURATE_LIMIT),
272
- ],
273
- AKM_CURATE_TIMEOUT_MS,
274
- )
264
+ function appendRunScopeArgs(args: string[], sessionID: string | undefined): string[] {
265
+ return sessionID ? [...args, "--run", sessionID] : args
266
+ }
267
+
268
+ function runCurate(args: string[]): string | null {
269
+ const result = runCliSyncRaw(args, AKM_CURATE_TIMEOUT_MS)
275
270
  if (!result.ok) return null
276
271
  const body = result.stdout.trim()
277
272
  return body || null
278
273
  }
279
274
 
275
+ function runCurateForPrompt(text: string, sessionID?: string): string | null {
276
+ if (!text || text.length < AKM_CURATE_MIN_CHARS) return null
277
+ return runCurate(
278
+ appendRunScopeArgs(
279
+ [
280
+ "--for-agent",
281
+ "--format",
282
+ "text",
283
+ "--detail",
284
+ "summary",
285
+ "-q",
286
+ "curate",
287
+ text,
288
+ "--limit",
289
+ String(AKM_CURATE_LIMIT),
290
+ ],
291
+ sessionID,
292
+ ),
293
+ )
294
+ }
295
+
296
+ function runCurateForSession(sessionID: string): string | null {
297
+ return runCurate(
298
+ appendRunScopeArgs(
299
+ [
300
+ "--for-agent",
301
+ "--format",
302
+ "text",
303
+ "--detail",
304
+ "summary",
305
+ "-q",
306
+ "curate",
307
+ "--limit",
308
+ String(AKM_CURATE_LIMIT),
309
+ ],
310
+ sessionID,
311
+ ),
312
+ )
313
+ }
314
+
280
315
  function runHintsForSession(): string | null {
281
316
  const result = runCliSyncRaw(["--format", "text", "-q", "hints"], AKM_CURATE_TIMEOUT_MS)
282
317
  if (!result.ok) return null
@@ -543,6 +578,25 @@ function maybeCheckpointSessionMemory(sessionID: string): string | null {
543
578
  return captured
544
579
  }
545
580
 
581
+ async function maybeIndexSessionMemory(
582
+ client: LogCapableClient,
583
+ sessionID: string,
584
+ reason: string,
585
+ ref: string,
586
+ ): Promise<void> {
587
+ if (!shouldIndexOnSessionEnd()) return
588
+ const result = runCliSyncRaw(["index"], AKM_CURATE_TIMEOUT_MS)
589
+ if (result.ok) return
590
+ await writePluginLog(client, "warn", "AKM session indexing failed", {
591
+ subsystem: "memory",
592
+ actor: "system",
593
+ sessionID,
594
+ reason,
595
+ ref,
596
+ error: result.error,
597
+ })
598
+ }
599
+
546
600
  const AKM_REF_EDGE_PUNCTUATION = new Set([".", ",", ";", ":", "!", "?", "(", ")", "[", "]", "{", "}", "'", "\"", "`"])
547
601
 
548
602
  function normalizeExtractedRef(ref: string): string {
@@ -614,6 +668,41 @@ const AKM_HINTS_PREFIX = [
614
668
 
615
669
  const AKM_CURATED_HEADER = "# AKM stash — assets relevant to this prompt"
616
670
  const AKM_CURATED_TAIL = "\n\nTip: call `akm_show <ref>` to fetch full content, and record `akm_feedback <ref> positive|negative` once you know whether the asset helped."
671
+ const AKM_CONTEXT_TRUNCATED_MARKER = "\n\n[truncated for context]"
672
+
673
+ function getContextBudgetChars(): number {
674
+ const parsed = Number(process.env.AKM_CONTEXT_BUDGET_CHARS)
675
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 4000
676
+ }
677
+
678
+ function truncateContextBlock(block: string, maxChars: number): string {
679
+ if (block.length <= maxChars) return block
680
+ if (maxChars <= AKM_CONTEXT_TRUNCATED_MARKER.length) return block.slice(0, maxChars)
681
+ return `${block.slice(0, maxChars - AKM_CONTEXT_TRUNCATED_MARKER.length)}${AKM_CONTEXT_TRUNCATED_MARKER}`
682
+ }
683
+
684
+ function applyContextBudget(blocks: string[]): string[] {
685
+ const budget = getContextBudgetChars()
686
+ const injected: string[] = []
687
+ let remaining = budget
688
+ for (const block of blocks) {
689
+ if (!block) continue
690
+ // The host effectively concatenates injected blocks into one prompt body;
691
+ // we budget for a single newline separator between adjacent blocks.
692
+ const separatorCost = injected.length > 0 ? 1 : 0
693
+ if (remaining <= separatorCost) break
694
+ const allowed = remaining - separatorCost
695
+ if (block.length <= allowed) {
696
+ injected.push(block)
697
+ remaining -= separatorCost + block.length
698
+ continue
699
+ }
700
+ const truncated = truncateContextBlock(block, allowed)
701
+ if (truncated) injected.push(truncated)
702
+ break
703
+ }
704
+ return injected
705
+ }
617
706
 
618
707
  // Curated quick-reference for the long-tail of `akm` CLI verbs that no longer
619
708
  // have a dedicated tool wrapper. Surfaced through akm_help so agents can
@@ -742,6 +831,72 @@ function getCommandStatus(command: string): "ok" | "missing" | "error" {
742
831
  }
743
832
  }
744
833
 
834
+ function extractFirstSemverMatch(value: string): string | null {
835
+ return value.match(SEMVER_PATTERN)?.[0] ?? null
836
+ }
837
+
838
+ function getCommandVersion(command: string): string | null {
839
+ try {
840
+ const version = execFileSync(command, ["--version"], {
841
+ encoding: "utf8",
842
+ timeout: 10_000,
843
+ })
844
+ return extractFirstSemverMatch(version)
845
+ } catch {
846
+ return null
847
+ }
848
+ }
849
+
850
+ function parseSemver(version: string): ParsedSemver | null {
851
+ const normalized = extractFirstSemverMatch(version)
852
+ if (!normalized) return null
853
+
854
+ const [withoutBuildMetadata] = normalized.split("+", 1)
855
+ const [release, prereleaseText] = withoutBuildMetadata.split("-", 2)
856
+ const parts = release.split(".").map((part) => Number(part))
857
+ if (parts.length !== 3 || parts.some((part) => !Number.isInteger(part) || part < 0)) {
858
+ return null
859
+ }
860
+ const core = [parts[0], parts[1], parts[2]] as [number, number, number]
861
+
862
+ return {
863
+ core,
864
+ prerelease: prereleaseText
865
+ ? prereleaseText.split(".").map((part) => (/^\d+$/.test(part) ? Number(part) : part))
866
+ : null,
867
+ }
868
+ }
869
+
870
+ function compareSemver(left: string, right: string): number {
871
+ const leftParsed = parseSemver(left)
872
+ const rightParsed = parseSemver(right)
873
+ if (!leftParsed || !rightParsed) return left.localeCompare(right)
874
+
875
+ for (let index = 0; index < leftParsed.core.length; index += 1) {
876
+ const delta = leftParsed.core[index] - rightParsed.core[index]
877
+ if (delta !== 0) return delta
878
+ }
879
+
880
+ if (!leftParsed.prerelease && !rightParsed.prerelease) return 0
881
+ if (!leftParsed.prerelease) return 1
882
+ if (!rightParsed.prerelease) return -1
883
+
884
+ const length = Math.max(leftParsed.prerelease.length, rightParsed.prerelease.length)
885
+ for (let index = 0; index < length; index += 1) {
886
+ const leftPart = leftParsed.prerelease[index]
887
+ const rightPart = rightParsed.prerelease[index]
888
+ if (leftPart === undefined) return -1
889
+ if (rightPart === undefined) return 1
890
+ if (leftPart === rightPart) continue
891
+ if (typeof leftPart === "number" && typeof rightPart === "number") return leftPart - rightPart
892
+ if (typeof leftPart === "number") return -1
893
+ if (typeof rightPart === "number") return 1
894
+ return leftPart.localeCompare(rightPart)
895
+ }
896
+
897
+ return 0
898
+ }
899
+
745
900
  function getBunGlobalAkmCommand(): string | null {
746
901
  try {
747
902
  const globalBin = execFileSync("bun", ["pm", "bin", "-g"], {
@@ -757,6 +912,39 @@ function getBunGlobalAkmCommand(): string | null {
757
912
  }
758
913
  }
759
914
 
915
+ function getInstalledAkmDetails(): { command: string; version: string } | null {
916
+ const candidates = [resolvedAkmCommand, getBunGlobalAkmCommand(), "akm"]
917
+ const seen = new Set<string>()
918
+ for (const candidate of candidates) {
919
+ if (!candidate || seen.has(candidate)) continue
920
+ seen.add(candidate)
921
+ const version = getCommandVersion(candidate)
922
+ if (version) return { command: candidate, version }
923
+ }
924
+ return null
925
+ }
926
+
927
+ async function getLatestNpmPackageVersion(packageName: string): Promise<string | null> {
928
+ if (typeof fetch !== "function") return null
929
+
930
+ const controller = new AbortController()
931
+ const timeout = setTimeout(() => controller.abort(), 10_000)
932
+ try {
933
+ const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
934
+ headers: { accept: "application/json" },
935
+ signal: controller.signal,
936
+ })
937
+ if (!response.ok) return null
938
+ const body = await response.json()
939
+ if (!body || typeof body !== "object") return null
940
+ return typeof body.version === "string" ? extractFirstSemverMatch(body.version) : null
941
+ } catch {
942
+ return null
943
+ } finally {
944
+ clearTimeout(timeout)
945
+ }
946
+ }
947
+
760
948
  async function ensureLatestAkmInstalled(client: LogCapableClient): Promise<void> {
761
949
  try {
762
950
  execFileSync("bun", ["--version"], {
@@ -772,6 +960,39 @@ async function ensureLatestAkmInstalled(client: LogCapableClient): Promise<void>
772
960
  return
773
961
  }
774
962
 
963
+ const installedAkm = getInstalledAkmDetails()
964
+ const latestStable = await getLatestNpmPackageVersion("akm-cli")
965
+
966
+ if (installedAkm) {
967
+ if (!latestStable) {
968
+ resolvedAkmCommand = installedAkm.command
969
+ await writePluginLog(client, "info", "AKM auto-install skipped", {
970
+ subsystem: "akm",
971
+ installer: "bun",
972
+ package: autoInstallPackageRef,
973
+ command: resolvedAkmCommand,
974
+ installedVersion: installedAkm.version,
975
+ latestStable,
976
+ reason: "latest_version_unavailable",
977
+ })
978
+ return
979
+ }
980
+
981
+ if (compareSemver(installedAkm.version, latestStable) >= 0) {
982
+ resolvedAkmCommand = installedAkm.command
983
+ await writePluginLog(client, "info", "AKM auto-install skipped", {
984
+ subsystem: "akm",
985
+ installer: "bun",
986
+ package: autoInstallPackageRef,
987
+ command: resolvedAkmCommand,
988
+ installedVersion: installedAkm.version,
989
+ latestStable,
990
+ reason: "installed_version_not_older",
991
+ })
992
+ return
993
+ }
994
+ }
995
+
775
996
  try {
776
997
  execFileSync("bun", ["install", "-g", autoInstallPackageRef], {
777
998
  encoding: "utf8",
@@ -791,6 +1012,8 @@ async function ensureLatestAkmInstalled(client: LogCapableClient): Promise<void>
791
1012
  installer: "bun",
792
1013
  package: autoInstallPackageRef,
793
1014
  command: resolvedAkmCommand,
1015
+ installedVersion: installedAkm?.version ?? null,
1016
+ latestStable,
794
1017
  })
795
1018
  } catch (error: unknown) {
796
1019
  await writePluginLog(client, "warn", "AKM auto-install failed", {
@@ -1416,7 +1639,16 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1416
1639
  if (type === "session.created" || type === "session.updated") {
1417
1640
  if (!sid) return
1418
1641
  if (!sessionContextEpoch.has(sid)) sessionContextEpoch.set(sid, 0)
1419
- if (type === "session.created") warmIndexInBackground()
1642
+ if (type === "session.created") {
1643
+ warmIndexInBackground()
1644
+ if (AKM_AUTO_CURATE && !sessionCurated.has(sid)) {
1645
+ const curated = runCurateForSession(sid)
1646
+ if (curated) {
1647
+ bumpCuratedVersion(sid)
1648
+ sessionCurated.set(sid, curated)
1649
+ }
1650
+ }
1651
+ }
1420
1652
  if (AKM_AUTO_HINTS && !sessionHints.has(sid)) {
1421
1653
  const hints = runHintsForSession()
1422
1654
  if (hints) sessionHints.set(sid, hints)
@@ -1435,6 +1667,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1435
1667
  reason: type,
1436
1668
  ref: captured,
1437
1669
  })
1670
+ await maybeIndexSessionMemory(logClient, sid, type, captured)
1438
1671
  }
1439
1672
  // Drop per-session state so a re-created session does not inherit
1440
1673
  // stale hints/curation.
@@ -1472,6 +1705,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1472
1705
  reason: "stop",
1473
1706
  ref: captured,
1474
1707
  })
1708
+ await maybeIndexSessionMemory(logClient, sid, "stop", captured)
1475
1709
  }
1476
1710
  } catch {
1477
1711
  // Best-effort only.
@@ -1483,14 +1717,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1483
1717
  if (!sid) return
1484
1718
  if (!Array.isArray(output.context)) return
1485
1719
  markContextEpochDirty(sid)
1486
- const hints = sessionHints.get(sid)
1487
- if (hints) output.context.push(`${AKM_HINTS_PREFIX}\n\n${hints}`)
1488
- const curated = sessionCurated.get(sid)
1489
- if (curated) output.context.push(`${AKM_CURATED_HEADER}\n${curated}${AKM_CURATED_TAIL}`)
1490
- const workflow = sessionWorkflow.get(sid)
1491
- if (workflow) output.context.push(formatWorkflowContext(workflow))
1492
- const curatorReport = sessionCuratorReport.get(sid)
1493
- if (curatorReport) output.context.push(formatCuratorReportContext(curatorReport))
1720
+ const blocks = [
1721
+ sessionHints.get(sid) ? `${AKM_HINTS_PREFIX}\n\n${sessionHints.get(sid)}` : "",
1722
+ sessionCurated.get(sid) ? `${AKM_CURATED_HEADER}\n${sessionCurated.get(sid)}${AKM_CURATED_TAIL}` : "",
1723
+ sessionWorkflow.get(sid) ? formatWorkflowContext(sessionWorkflow.get(sid)!) : "",
1724
+ sessionCuratorReport.get(sid) ? formatCuratorReportContext(sessionCuratorReport.get(sid)!) : "",
1725
+ ]
1726
+ output.context.push(...applyContextBudget(blocks))
1494
1727
  } catch {
1495
1728
  // Never break compaction because of plugin context.
1496
1729
  }
@@ -1508,19 +1741,25 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1508
1741
  const epoch = sessionContextEpoch.get(sid) ?? 0
1509
1742
  const injectedEpoch = sessionContextInjectedEpoch.get(sid)
1510
1743
  if (sid && injectedEpoch !== epoch) {
1511
- const hints = sessionHints.get(sid)
1512
- if (hints) output.system.push(`${AKM_HINTS_PREFIX}\n\n${hints}`)
1513
- const workflow = sessionWorkflow.get(sid)
1514
- if (workflow) output.system.push(formatWorkflowContext(workflow))
1515
- const curatorReport = sessionCuratorReport.get(sid)
1516
- if (curatorReport) output.system.push(formatCuratorReportContext(curatorReport))
1744
+ const blocks = [
1745
+ sessionHints.get(sid) ? `${AKM_HINTS_PREFIX}\n\n${sessionHints.get(sid)}` : "",
1746
+ sessionCurated.get(sid) ? `${AKM_CURATED_HEADER}\n${sessionCurated.get(sid)}${AKM_CURATED_TAIL}` : "",
1747
+ sessionWorkflow.get(sid) ? formatWorkflowContext(sessionWorkflow.get(sid)!) : "",
1748
+ sessionCuratorReport.get(sid) ? formatCuratorReportContext(sessionCuratorReport.get(sid)!) : "",
1749
+ ]
1750
+ output.system.push(...applyContextBudget(blocks))
1517
1751
  sessionContextInjectedEpoch.set(sid, epoch)
1752
+ if (sessionCurated.has(sid)) {
1753
+ // Startup curation is already included in the epoch-scoped block,
1754
+ // so mark that version as consumed to avoid a duplicate inject below.
1755
+ sessionCuratedInjectedVersion.set(sid, sessionCuratedVersion.get(sid) ?? 0)
1756
+ }
1518
1757
  }
1519
1758
  const curated = sid ? sessionCurated.get(sid) : undefined
1520
1759
  const curatedVersion = sessionCuratedVersion.get(sid) ?? 0
1521
1760
  if (curated) {
1522
1761
  if (sessionCuratedInjectedVersion.get(sid) !== curatedVersion) {
1523
- output.system.push(`${AKM_CURATED_HEADER}\n${curated}${AKM_CURATED_TAIL}`)
1762
+ output.system.push(...applyContextBudget([`${AKM_CURATED_HEADER}\n${curated}${AKM_CURATED_TAIL}`]))
1524
1763
  sessionCuratedInjectedVersion.set(sid, curatedVersion)
1525
1764
  }
1526
1765
  }
@@ -1570,7 +1809,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1570
1809
  // Compound-engineering loop: on every user message, curate the stash and
1571
1810
  // stash the result so experimental.chat.system.transform can inject it.
1572
1811
  if (AKM_AUTO_CURATE && input.sessionID) {
1573
- const curated = runCurateForPrompt(text)
1812
+ const curated = runCurateForPrompt(text, input.sessionID)
1574
1813
  if (curated) {
1575
1814
  sessionCurated.set(input.sessionID, curated)
1576
1815
  bumpCuratedVersion(input.sessionID)
@@ -1771,18 +2010,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1771
2010
  detail: tool.schema.enum(["summary", "normal", "full"]).optional().describe("Detail level for each match. Defaults to 'summary'."),
1772
2011
  },
1773
2012
  async execute({ query, limit, detail }) {
1774
- const args = [
1775
- "--for-agent",
1776
- "--format",
1777
- "text",
1778
- "--detail",
1779
- detail ?? "summary",
1780
- "-q",
1781
- "curate",
1782
- query,
1783
- "--limit",
1784
- String(limit ?? 6),
1785
- ]
2013
+ const args = ["curate", query, "--limit", String(limit ?? 6)]
2014
+ if (detail) args.push("--detail", detail)
1786
2015
  return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_curate" })
1787
2016
  },
1788
2017
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-opencode",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for AKM - search, show, and manage extension assets via the akm CLI, including v0.5.0 vaults, wikis, and workflows, with agentic hooks that auto-load relevant stash assets, record feedback, and harvest session memories so the stash improves every session.",
6
6
  "keywords": [