akm-cli 0.9.0-beta.47 → 0.9.0-beta.49

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.
@@ -18,7 +18,7 @@ import { parseAssetRef } from "../../core/asset/asset-ref.js";
18
18
  import { NotFoundError, UsageError } from "../../core/errors.js";
19
19
  import { requireAgentProfile } from "../../integrations/agent/config.js";
20
20
  import { runAgent } from "../../integrations/agent/spawn.js";
21
- import { runAgentSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
21
+ import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
22
22
  /**
23
23
  * Fill `{{0}}`, `{{1}}`, ... placeholders in `template` with the
24
24
  * corresponding entries in `args`. Any placeholder index that exceeds the
@@ -93,7 +93,7 @@ export async function akmAgentDispatch(options) {
93
93
  ...(dispatchRequest !== undefined ? { dispatch: dispatchRequest } : {}),
94
94
  };
95
95
  const result = profile.sdkMode
96
- ? await runAgentSdk(profile, prompt ?? "", runOptions, options.llmConfig)
96
+ ? await runOpencodeSdk(profile, prompt ?? "", runOptions, options.llmConfig)
97
97
  : await runAgent(profile, prompt, runOptions);
98
98
  return {
99
99
  schemaVersion: 1,
@@ -1,13 +1,6 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- /**
5
- * Shared helpers for agent-based commands (reflect, propose, etc.).
6
- *
7
- * Consolidates utility functions that were duplicated byte-for-byte across
8
- * `reflect.ts` and `propose.ts`. Any command that shells out to an agent
9
- * profile can import from here rather than copy-pasting.
10
- */
11
4
  import { loadConfig } from "../../core/config/config.js";
12
5
  import { requireAgentProfile, } from "../../integrations/agent/index.js";
13
6
  // ── Config helpers ───────────────────────────────────────────────────────────
@@ -29,7 +29,7 @@ import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "
29
29
  import { loadConfig } from "../../core/config/config.js";
30
30
  import { UsageError } from "../../core/errors.js";
31
31
  import { getHyphenatedArg } from "../../output/context.js";
32
- import { akmLint } from "../lint.js";
32
+ import { akmLint } from "../lint/index.js";
33
33
  import { akmPropose } from "../proposal/propose.js";
34
34
  import { akmAgentDispatch } from "./agent-dispatch.js";
35
35
  const EXIT_GENERAL = EXIT_CODES.GENERAL;
@@ -22,7 +22,7 @@ import { resolveStandardsContext } from "../../core/standards/resolve-standards-
22
22
  import { runAgent, } from "../../integrations/agent/index.js";
23
23
  import { resolveProcessAgentProfile } from "../../integrations/agent/config.js";
24
24
  import { buildProposePrompt, parseAgentProposalPayload } from "../../integrations/agent/prompts.js";
25
- import { runAgentSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
25
+ import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
26
26
  import { baseFailureFields, enoentHintMessage, isEnoentFailure, loadAgentConfigFromDisk, resolveAgentProfile, } from "../agent/agent-support.js";
27
27
  import { createProposal, isProposalSkipped, } from "./validators/proposals.js";
28
28
  function failureEnvelope(result, type, name, fallbackReason = "non_zero_exit") {
@@ -128,7 +128,7 @@ export async function akmPropose(options) {
128
128
  ...(resolvedTimeoutMs !== undefined ? { timeoutMs: resolvedTimeoutMs } : {}),
129
129
  };
130
130
  result = profile.sdkMode
131
- ? await runAgentSdk(profile, prompt ?? "", runOptions)
131
+ ? await runOpencodeSdk(profile, prompt ?? "", runOptions)
132
132
  : await runAgent(profile, prompt, runOptions);
133
133
  }
134
134
  if (!result.ok) {
@@ -20,7 +20,7 @@ import { listAgentProfileNames } from "../../integrations/agent/index.js";
20
20
  import { resolveAssetPath } from "../../sources/resolve.js";
21
21
  import { backendNameForPlatform, selectBackend } from "../../tasks/backends/index.js";
22
22
  import { parseTaskDocument } from "../../tasks/parser.js";
23
- import { resolveAkmInvocation } from "../../tasks/resolveAkmBin.js";
23
+ import { resolveAkmInvocation } from "../../tasks/resolve-akm-bin.js";
24
24
  import { exitCodeForStatus, readTaskHistory, runTask } from "../../tasks/runner.js";
25
25
  import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT, translateToCron } from "../../tasks/schedule.js";
26
26
  import { validateTaskDocument } from "../../tasks/validator.js";
@@ -44,11 +44,6 @@ export function isRemoteUrl(value) {
44
44
  value.startsWith("ssh://") ||
45
45
  value.startsWith("git://"));
46
46
  }
47
- export function filterNonEmptyStrings(value) {
48
- if (!Array.isArray(value))
49
- return undefined;
50
- return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
51
- }
52
47
  // ── Validators ──────────────────────────────────────────────────────────────
53
48
  /**
54
49
  * Returns true if `type` is a known asset type — either a built-in from
@@ -22,8 +22,14 @@
22
22
  * - Two exceptions (hard-rejected): openviking source type and legacy
23
23
  * `stashes[]` key. Both have explicit migration paths; silently dropping
24
24
  * would mask user data loss.
25
- * - `.strict()` walls still gate `registries[]`, `sources[]`, `profiles.*`
26
- * sub-shapes so typos in those structured records are caught (#462).
25
+ * - UNKNOWN-KEY POLICY: object schemas use passthrough (unknown keys are
26
+ * preserved and ignored, NOT rejected). akm runs across multiple installed
27
+ * versions sharing one config.json; a newer version writes keys an older
28
+ * version's schema doesn't know yet, so hard-rejecting unknown keys turned
29
+ * benign version skew into `INVALID_CONFIG_FILE` failures. Known keys are
30
+ * still type-checked; passthrough preserves unknown keys across a
31
+ * load→save round trip so an older reader never strips a newer writer's
32
+ * settings. (Replaced the prior strict-mode object walls.)
27
33
  * - `defaultWriteTarget` resolution and similar cross-field invariants are
28
34
  * enforced at save time via `superRefine` on the top-level schema.
29
35
  */
@@ -50,7 +56,7 @@ const LlmCapabilitiesSchema = z
50
56
  .object({
51
57
  structuredOutput: z.boolean().optional(),
52
58
  })
53
- .strict();
59
+ .passthrough();
54
60
  /**
55
61
  * Connection config used for both top-level `llm` (after migration) and
56
62
  * `profiles.llm[*]`. `model` is required at schema level — partial entries
@@ -75,15 +81,15 @@ export const LlmConnectionConfigSchema = z
75
81
  judgeModel: z.string().min(1).optional(),
76
82
  enableThinking: z.boolean().optional(),
77
83
  })
78
- .strict();
84
+ .passthrough();
79
85
  export const LlmProfileConfigSchema = LlmConnectionConfigSchema.extend({
80
86
  supportsJsonSchema: z.boolean().optional(),
81
- }).strict();
87
+ }).passthrough();
82
88
  const EmbeddingOllamaOptionsSchema = z
83
89
  .object({
84
90
  num_ctx: positiveInt.optional(),
85
91
  })
86
- .strict();
92
+ .passthrough();
87
93
  /**
88
94
  * Embedding connection config. Both `endpoint` and `model` are optional:
89
95
  * - Remote: provide `endpoint` (http/https URL) + `model`.
@@ -107,7 +113,7 @@ export const EmbeddingConnectionConfigSchema = z
107
113
  contextLength: positiveInt.optional(),
108
114
  ollamaOptions: EmbeddingOllamaOptionsSchema.optional(),
109
115
  })
110
- .strict();
116
+ .passthrough();
111
117
  // ── Agent profiles ──────────────────────────────────────────────────────────
112
118
  // Derives from the canonical VALID_HARNESS_IDS (#565) so the Zod gate cannot
113
119
  // drift from the TS union / parse check / setup detection.
@@ -120,7 +126,7 @@ export const AgentProfileConfigSchema = z
120
126
  workspace: z.string().min(1).optional(),
121
127
  model: z.string().min(1).optional(),
122
128
  })
123
- .strict();
129
+ .passthrough();
124
130
  // ── Improve profile / process ──────────────────────────────────────────────
125
131
  export const ImproveProcessConfigSchema = z
126
132
  .object({
@@ -151,7 +157,7 @@ export const ImproveProcessConfigSchema = z
151
157
  // with care — cost is O(n²).
152
158
  cosineCandidateLimit: z.number().int().positive().optional(),
153
159
  })
154
- .strict()
160
+ .passthrough()
155
161
  .optional(),
156
162
  // Consolidate process: judged-state cache (#581). When enabled, a memory
157
163
  // whose current content hash equals its cached judged hash is SKIPPED from
@@ -160,9 +166,9 @@ export const ImproveProcessConfigSchema = z
160
166
  // time-window slice. Default OFF — when absent the consolidate pass behaves
161
167
  // byte-identically to today (the incrementalSince path is unaffected). Only
162
168
  // meaningful on the `consolidate` process.
163
- judgedCache: z.object({ enabled: z.boolean().optional() }).strict().optional(),
164
- qualityGate: z.object({ enabled: z.boolean().optional() }).strict().optional(),
165
- contradictionDetection: z.object({ enabled: z.boolean().optional() }).strict().optional(),
169
+ judgedCache: z.object({ enabled: z.boolean().optional() }).passthrough().optional(),
170
+ qualityGate: z.object({ enabled: z.boolean().optional() }).passthrough().optional(),
171
+ contradictionDetection: z.object({ enabled: z.boolean().optional() }).passthrough().optional(),
166
172
  // Extract process config (only meaningful for extract process)
167
173
  defaultSince: z.string().min(1).optional(),
168
174
  maxTotalChars: positiveInt.optional(),
@@ -236,7 +242,7 @@ export const ImproveProcessConfigSchema = z
236
242
  // Demotion factor: multiply retrievalSalience by this when stale (default 0.5).
237
243
  demotionFactor: z.number().min(0).max(1).optional(),
238
244
  })
239
- .strict()
245
+ .passthrough()
240
246
  .optional(),
241
247
  // WS-3b: Schema-similarity gate (step 0b). At intake, if a new candidate's
242
248
  // body embedding is within epsilon of an existing derived-layer lesson/knowledge
@@ -253,7 +259,7 @@ export const ImproveProcessConfigSchema = z
253
259
  // to pass the quality gate and create redundant stash entries.
254
260
  confidencePenalty: z.number().min(0).max(1).optional(),
255
261
  })
256
- .strict()
262
+ .passthrough()
257
263
  .optional(),
258
264
  // WS-3b: Hot-probation intake buffer (step 0c, #604). New system-generated
259
265
  // extractions enter captureMode: hot-probation and spend ONE consolidation
@@ -263,7 +269,7 @@ export const ImproveProcessConfigSchema = z
263
269
  .object({
264
270
  enabled: z.boolean().optional(),
265
271
  })
266
- .strict()
272
+ .passthrough()
267
273
  .optional(),
268
274
  // WS-3b: Anti-collapse guards (step 8). Prevents the consolidation pipeline
269
275
  // from collapsing too aggressively and losing diversity.
@@ -278,7 +284,7 @@ export const ImproveProcessConfigSchema = z
278
284
  lexicalDiversityCheck: z.boolean().optional(),
279
285
  randomClusterFraction: z.number().min(0).max(1).optional(),
280
286
  })
281
- .strict()
287
+ .passthrough()
282
288
  .optional(),
283
289
  // WS-3b: CLS (Complementary Learning System) interleaving (step 9).
284
290
  // distill/memoryInference prompts include embedding-retrieved existing adjacent
@@ -290,7 +296,7 @@ export const ImproveProcessConfigSchema = z
290
296
  // Number of adjacent lessons/knowledge to include in prompts (default 3).
291
297
  adjacentCount: z.number().int().min(1).optional(),
292
298
  })
293
- .strict()
299
+ .passthrough()
294
300
  .optional(),
295
301
  // WS-3b: Distill→source fidelity check (step 10). After a distill proposal,
296
302
  // check it against its cited source memories; a contradiction flag forces
@@ -299,7 +305,7 @@ export const ImproveProcessConfigSchema = z
299
305
  .object({
300
306
  enabled: z.boolean().optional(),
301
307
  })
302
- .strict()
308
+ .passthrough()
303
309
  .optional(),
304
310
  // #609 — recombine process: minimum related-memory cluster size before an
305
311
  // LLM generalization call. Default 3. Only meaningful on `recombine`.
@@ -342,7 +348,7 @@ export const ImproveProcessConfigSchema = z
342
348
  // enabled, proposals classified as "low-value" by the deterministic noise
343
349
  // gate are deferred. DEFAULT OFF (absent / { enabled: false } = pre-#639
344
350
  // byte-identical behaviour). Only meaningful on the `reflect` process.
345
- lowValueFilter: z.object({ enabled: z.boolean().optional() }).strict().optional(),
351
+ lowValueFilter: z.object({ enabled: z.boolean().optional() }).passthrough().optional(),
346
352
  // #641 — procedural-aware floor for the `extract` process triage gate.
347
353
  // When true, a session must have markers>=1 OR editCommit>=0.5 to pass, even
348
354
  // if score>=minScore. DEFAULT OFF (absent/false = pre-#641 byte-identical).
@@ -360,10 +366,10 @@ export const ImproveProcessConfigSchema = z
360
366
  profile: z.string().min(1).optional(),
361
367
  timeoutMs: z.union([positiveInt, z.null()]).optional(),
362
368
  })
363
- .strict()
369
+ .passthrough()
364
370
  .optional(),
365
371
  })
366
- .strict();
372
+ .passthrough();
367
373
  const ImproveProfileProcessesSchema = z
368
374
  .object({
369
375
  reflect: ImproveProcessConfigSchema.optional(),
@@ -380,37 +386,17 @@ const ImproveProfileProcessesSchema = z
380
386
  .passthrough()
381
387
  .superRefine((val, ctx) => {
382
388
  // 0.8.0 removed the duplicated `feedbackDistillation` process key — it was
383
- // a thin wrapper around `processes.distill.enabled`. Single source of truth.
384
- const raw = val;
385
- if ("feedbackDistillation" in raw) {
389
+ // a thin wrapper around `processes.distill.enabled`. Keep the migration
390
+ // hint so a stale config gets an actionable message rather than silently
391
+ // doing nothing. All OTHER unknown process keys are tolerated (passthrough)
392
+ // — see the unknown-key policy in this file's header. Hard-rejecting them
393
+ // turned benign cross-version skew into INVALID_CONFIG_FILE failures.
394
+ if ("feedbackDistillation" in val) {
386
395
  ctx.addIssue({
387
396
  code: z.ZodIssueCode.custom,
388
397
  message: "feedbackDistillation was removed in 0.8.0 — use processes.distill.enabled instead. " +
389
398
  "It now controls both the orchestration gate and the LLM-call gate.",
390
399
  });
391
- return;
392
- }
393
- const allowed = new Set([
394
- "reflect",
395
- "distill",
396
- "consolidate",
397
- "memoryInference",
398
- "graphExtraction",
399
- "validation",
400
- "extract",
401
- "triage",
402
- "proactiveMaintenance",
403
- "recombine",
404
- "procedural",
405
- ]);
406
- for (const k of Object.keys(raw)) {
407
- if (!allowed.has(k)) {
408
- ctx.addIssue({
409
- code: z.ZodIssueCode.unrecognized_keys,
410
- keys: [k],
411
- message: `Unrecognized improve process key: "${k}".`,
412
- });
413
- }
414
400
  }
415
401
  });
416
402
  export const ImproveProfileConfigSchema = z
@@ -435,10 +421,10 @@ export const ImproveProfileConfigSchema = z
435
421
  push: z.boolean().optional(),
436
422
  message: z.string().min(1).optional(),
437
423
  })
438
- .strict()
424
+ .passthrough()
439
425
  .optional(),
440
426
  })
441
- .strict();
427
+ .passthrough();
442
428
  // ── Profiles / defaults ────────────────────────────────────────────────────
443
429
  export const ProfilesSchema = z
444
430
  .object({
@@ -446,14 +432,14 @@ export const ProfilesSchema = z
446
432
  agent: z.record(z.string(), AgentProfileConfigSchema).optional(),
447
433
  improve: z.record(z.string(), ImproveProfileConfigSchema).optional(),
448
434
  })
449
- .strict();
435
+ .passthrough();
450
436
  export const DefaultsSchema = z
451
437
  .object({
452
438
  llm: z.string().min(1).optional(),
453
439
  agent: z.string().min(1).optional(),
454
440
  improve: z.string().min(1).optional(),
455
441
  })
456
- .strict();
442
+ .passthrough();
457
443
  // ── Sources / registries / installed ────────────────────────────────────────
458
444
  const SourceConfigEntryOptionsSchema = z
459
445
  .object({
@@ -477,7 +463,7 @@ export const SourceConfigEntrySchema = z
477
463
  options: SourceConfigEntryOptionsSchema.optional(),
478
464
  wikiName: z.string().min(1).optional(),
479
465
  })
480
- .strict()
466
+ .passthrough()
481
467
  .superRefine((entry, ctx) => {
482
468
  if (entry.writable === true && (entry.type === "website" || entry.type === "npm")) {
483
469
  ctx.addIssue({
@@ -496,7 +482,7 @@ export const RegistryConfigEntrySchema = z
496
482
  provider: z.string().min(1).optional(),
497
483
  options: z.record(z.unknown()).optional(),
498
484
  })
499
- .strict();
485
+ .passthrough();
500
486
  const KitSourceSchema = z.enum(["filesystem", "git", "npm", "github", "website", "local"]);
501
487
  export const InstalledStashEntrySchema = z
502
488
  .object({
@@ -512,7 +498,7 @@ export const InstalledStashEntrySchema = z
512
498
  resolvedRevision: z.string().min(1).optional(),
513
499
  wikiName: z.string().min(1).optional(),
514
500
  })
515
- .strict()
501
+ .passthrough()
516
502
  .superRefine((entry, ctx) => {
517
503
  if (entry.writable === true && entry.source !== "git" && entry.source !== "filesystem") {
518
504
  ctx.addIssue({
@@ -527,7 +513,7 @@ export const OutputConfigSchema = z
527
513
  format: z.enum(["json", "yaml", "text"]).optional(),
528
514
  detail: z.enum(["brief", "normal", "full"]).optional(),
529
515
  })
530
- .strict();
516
+ .passthrough();
531
517
  // ── Search ──────────────────────────────────────────────────────────────────
532
518
  const SearchGraphBoostSchema = z
533
519
  .object({
@@ -541,29 +527,29 @@ const SearchGraphBoostSchema = z
541
527
  /** Range [0, 1]; values > 1 hard-error (no silent clamp). */
542
528
  confidenceWeight: z.number().finite().min(0).max(1).default(0.2).optional(),
543
529
  })
544
- .strict();
530
+ .passthrough();
545
531
  export const SearchConfigSchema = z
546
532
  .object({
547
533
  minScore: nonNegativeNumber.optional(),
548
534
  defaultExcludeTypes: z.array(nonEmptyString).optional(),
549
- curateRerank: z.object({ enabled: z.boolean().optional() }).strict().optional(),
535
+ curateRerank: z.object({ enabled: z.boolean().optional() }).passthrough().optional(),
550
536
  graphBoost: SearchGraphBoostSchema.optional(),
551
537
  })
552
- .strict();
538
+ .passthrough();
553
539
  // ── Feedback ────────────────────────────────────────────────────────────────
554
540
  export const FeedbackConfigSchema = z
555
541
  .object({
556
542
  requireReason: z.boolean().optional(),
557
543
  allowedFailureModes: z.array(nonEmptyString).optional(),
558
544
  })
559
- .strict();
545
+ .passthrough();
560
546
  // ── Improve top-level (utility decay, event retention) ─────────────────────
561
547
  const ImproveUtilityDecaySchema = z
562
548
  .object({
563
549
  halfLifeDays: z.number().finite().min(0.1).optional(),
564
550
  feedbackStabilityBoost: z.number().finite().min(1).optional(),
565
551
  })
566
- .strict();
552
+ .passthrough();
567
553
  // #612 / WS-4 — auto-accept gate calibration + bounded, opt-in per-phase
568
554
  // threshold auto-tune. DEFAULT OFF: when absent (or `autoTune: false`) no
569
555
  // tuning occurs, so the gate behaves byte-identically to today.
@@ -586,7 +572,7 @@ const ImproveCalibrationSchema = z
586
572
  /** Target realized accept rate in [0, 1]. Default 0.9. */
587
573
  targetAcceptRate: z.number().finite().min(0).max(1).optional(),
588
574
  })
589
- .strict();
575
+ .passthrough();
590
576
  // WS-4 — exploration budget: a fixed fraction of proposals accepted per run
591
577
  // regardless of confidence. DEFAULT OFF.
592
578
  const ImproveExplorationSchema = z
@@ -602,7 +588,7 @@ const ImproveExplorationSchema = z
602
588
  */
603
589
  budgetFraction: z.number().finite().min(0).max(1).optional(),
604
590
  })
605
- .strict();
591
+ .passthrough();
606
592
  const ImproveSalienceSchema = z
607
593
  .object({
608
594
  /**
@@ -624,7 +610,7 @@ const ImproveSalienceSchema = z
624
610
  */
625
611
  replayBudget: z.number().int().min(0).optional(),
626
612
  })
627
- .strict();
613
+ .passthrough();
628
614
  export const ImproveConfigSchema = z
629
615
  .object({
630
616
  utilityDecay: ImproveUtilityDecaySchema.optional(),
@@ -633,7 +619,7 @@ export const ImproveConfigSchema = z
633
619
  exploration: ImproveExplorationSchema.optional(),
634
620
  salience: ImproveSalienceSchema.optional(),
635
621
  })
636
- .strict();
622
+ .passthrough();
637
623
  // ── Index / per-pass ────────────────────────────────────────────────────────
638
624
  const GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED = [
639
625
  "memory",
@@ -712,13 +698,13 @@ export const IndexPassConfigSchema = z.preprocess((raw, ctx) => {
712
698
  lazyGraphExtraction: z.boolean().optional(),
713
699
  })
714
700
  .passthrough());
715
- const MetadataEnhanceSchema = z.object({ enabled: z.boolean().optional() }).strict();
701
+ const MetadataEnhanceSchema = z.object({ enabled: z.boolean().optional() }).passthrough();
716
702
  const StalenessDetectionSchema = z
717
703
  .object({
718
704
  enabled: z.boolean().optional(),
719
705
  thresholdDays: positiveInt.optional(),
720
706
  })
721
- .strict();
707
+ .passthrough();
722
708
  /**
723
709
  * Index config is a union of reserved feature sections and per-pass entries.
724
710
  * Passthrough so per-pass entries (keyed by arbitrary pass names like `graph`,
@@ -791,12 +777,12 @@ export const SetupTaskSchedulesSchema = z
791
777
  improve: z.string().min(1).optional(),
792
778
  index: z.string().min(1).optional(),
793
779
  })
794
- .strict();
780
+ .passthrough();
795
781
  export const SetupConfigSchema = z
796
782
  .object({
797
783
  taskSchedules: SetupTaskSchedulesSchema.optional(),
798
784
  })
799
- .strict();
785
+ .passthrough();
800
786
  // ── Top-level AkmConfig ────────────────────────────────────────────────────
801
787
  /**
802
788
  * Base object schema used both as the top-level shape and as the source of
@@ -828,7 +814,7 @@ export const AkmConfigShape = {
828
814
  improve: ImproveConfigSchema.optional(),
829
815
  setup: SetupConfigSchema.optional(),
830
816
  };
831
- export const AkmConfigBaseSchema = z.object(AkmConfigShape).strict();
817
+ export const AkmConfigBaseSchema = z.object(AkmConfigShape).passthrough();
832
818
  export const AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) => {
833
819
  // #464.a: defaultWriteTarget must name a configured source when sources
834
820
  // are present. With no sources configured, error out instead of silently
@@ -9,8 +9,8 @@ import { VALID_HARNESS_IDS } from "../../integrations/harnesses/index.js";
9
9
  /**
10
10
  * Canonical list of valid agent harness / platform ids. Re-exported from the
11
11
  * unified harness registry (#562) so the Zod `AgentPlatformSchema` enum, the
12
- * `AgentProfileConfigV2` platform union, `parseAgentProfilesMapV2`'s membership
13
- * check, and setup's `DetectedHarness` union all derive from one place and
14
- * cannot drift. Add a harness in `src/integrations/harnesses/index.ts`.
12
+ * `AgentProfileConfig` platform union, and setup's `DetectedHarness` union all
13
+ * derive from one place and cannot drift. Add a harness in
14
+ * `src/integrations/harnesses/index.ts`.
15
15
  */
16
16
  export { VALID_HARNESS_IDS };
@@ -469,22 +469,70 @@ export function insertProposalIfAbsent(db, proposal, stashDir) {
469
469
  * need the write lock BEFORE those reads so concurrent processes serialize on
470
470
  * the live queue state rather than clobbering each other.
471
471
  */
472
+ /**
473
+ * Errors `BEGIN IMMEDIATE` can throw under concurrent-writer contention that are
474
+ * transient (the statement did NOT start a usable transaction) and safe to
475
+ * retry after clearing any phantom transaction state:
476
+ * - "database is locked" / SQLITE_BUSY — another writer holds the lock.
477
+ * - "cannot start a transaction within a transaction" — bun:sqlite can leave
478
+ * the connection reporting an open transaction after a contended busy-wait
479
+ * on BEGIN IMMEDIATE (observed only under heavy parallel load, e.g. the
480
+ * proposal-queue worker race). A ROLLBACK clears that phantom state.
481
+ * These are start-of-transaction failures only; an error thrown by `fn` is a
482
+ * real failure and is NEVER retried.
483
+ */
484
+ function isRetryableBeginError(err) {
485
+ const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
486
+ return (msg.includes("within a transaction") ||
487
+ msg.includes("database is locked") ||
488
+ msg.includes("database table is locked"));
489
+ }
490
+ const WITH_IMMEDIATE_TX_MAX_ATTEMPTS = 5;
491
+ /** Portable synchronous sleep (works under both Bun and Node). */
492
+ function sleepSyncMs(ms) {
493
+ if (ms <= 0)
494
+ return;
495
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
496
+ }
472
497
  export function withImmediateTransaction(db, fn) {
473
- db.exec("BEGIN IMMEDIATE");
474
- try {
475
- const result = fn();
476
- db.exec("COMMIT");
477
- return result;
478
- }
479
- catch (err) {
498
+ let lastBeginErr;
499
+ for (let attempt = 1; attempt <= WITH_IMMEDIATE_TX_MAX_ATTEMPTS; attempt++) {
480
500
  try {
481
- db.exec("ROLLBACK");
501
+ db.exec("BEGIN IMMEDIATE");
482
502
  }
483
- catch {
484
- // Ignore rollback failures so the original error is preserved.
503
+ catch (err) {
504
+ lastBeginErr = err;
505
+ if (isRetryableBeginError(err) && attempt < WITH_IMMEDIATE_TX_MAX_ATTEMPTS) {
506
+ // Clear any phantom/stale transaction left by the contended BEGIN, then
507
+ // retry with a small backoff so concurrent writers serialize cleanly.
508
+ try {
509
+ db.exec("ROLLBACK");
510
+ }
511
+ catch {
512
+ // No active transaction to roll back — fine.
513
+ }
514
+ sleepSyncMs(2 ** (attempt - 1));
515
+ continue;
516
+ }
517
+ throw err;
518
+ }
519
+ try {
520
+ const result = fn();
521
+ db.exec("COMMIT");
522
+ return result;
523
+ }
524
+ catch (err) {
525
+ try {
526
+ db.exec("ROLLBACK");
527
+ }
528
+ catch {
529
+ // Ignore rollback failures so the original error is preserved.
530
+ }
531
+ throw err; // a real error inside the transaction body — never retried.
485
532
  }
486
- throw err;
487
533
  }
534
+ // Exhausted retries on transient begin failures.
535
+ throw lastBeginErr;
488
536
  }
489
537
  // ── task_history table helpers ───────────────────────────────────────────────
490
538
  /**
@@ -1,9 +1,7 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import { VALID_HARNESS_IDS } from "../../core/config/config.js";
5
4
  import { ConfigError } from "../../core/errors.js";
6
- import { warn } from "../../core/warn.js";
7
5
  import { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile, listBuiltinAgentProfiles, } from "./profiles.js";
8
6
  /**
9
7
  * Default hard timeout for an agent CLI (60s — matches the value used in
@@ -41,7 +39,12 @@ export function resolveAgentProfile(name, overrides) {
41
39
  stdio: base.stdio,
42
40
  env: base.env,
43
41
  envPassthrough: base.envPassthrough,
44
- timeoutMs: base.timeoutMs,
42
+ // Honor a user-configured `profiles.agent.<name>.timeoutMs` override; fall
43
+ // back to the built-in profile's value. (Previously always used base, so
44
+ // the documented config override was silently ignored — callers had to pass
45
+ // a CLI flag like `--timeout-ms`.) runAgent (spawn.ts) reads profile.timeoutMs
46
+ // when no per-call timeout is supplied, so this makes the config knob work.
47
+ timeoutMs: overrides.timeoutMs ?? base.timeoutMs,
45
48
  parseOutput: base.parseOutput,
46
49
  ...(sdkMode ? { sdkMode: true } : {}),
47
50
  model: overrides.model ?? base.model,
@@ -134,53 +137,3 @@ export function listResolvedAgentProfiles(config) {
134
137
  }
135
138
  return resolved;
136
139
  }
137
- /**
138
- * Parse the v2 `profiles.agent` map (AgentProfileConfigV2 shape with required
139
- * `platform` field). Returns a map of profile name → AgentProfileConfigV2.
140
- */
141
- export function parseAgentProfilesMapV2(value) {
142
- if (typeof value !== "object" || value === null || Array.isArray(value))
143
- return undefined;
144
- const out = {};
145
- // Derives from the canonical harness-id source of truth (#565).
146
- const VALID_PLATFORMS = VALID_HARNESS_IDS;
147
- for (const [name, raw] of Object.entries(value)) {
148
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
149
- warn(`[akm] Ignoring profiles.agent["${name}"]: expected an object.`);
150
- continue;
151
- }
152
- const obj = raw;
153
- if (!VALID_PLATFORMS.includes(obj.platform)) {
154
- warn(`[akm] Ignoring profiles.agent["${name}"]: missing or invalid "platform" (must be one of: ${VALID_PLATFORMS.join(", ")}).`);
155
- continue;
156
- }
157
- const profile = {
158
- platform: obj.platform,
159
- };
160
- if (typeof obj.bin === "string" && obj.bin.trim())
161
- profile.bin = obj.bin.trim();
162
- if (Array.isArray(obj.args) && obj.args.every((a) => typeof a === "string")) {
163
- profile.args = obj.args;
164
- }
165
- if (typeof obj.workspace === "string" && obj.workspace.trim())
166
- profile.workspace = obj.workspace.trim();
167
- if (typeof obj.model === "string" && obj.model.trim())
168
- profile.model = obj.model.trim();
169
- out[name] = profile;
170
- }
171
- return Object.keys(out).length > 0 ? out : undefined;
172
- }
173
- /**
174
- * Stub kept for source-compat with callers that previously used the v1 agent
175
- * config parser. After 0.8.0 there is no separate `agent` block to parse — the
176
- * loaded `AkmConfig` already carries the agent data on `profiles.agent` and
177
- * `defaults.agent`. This function is a no-op alias for those callers.
178
- *
179
- * @deprecated v0.8.0 — the unified `AkmConfig` IS the agent config. Use the
180
- * profile/defaults accessors above instead.
181
- */
182
- export function parseAgentConfig(_value) {
183
- // No-op: there is no separate agent block in 0.8.0. Callers should pass
184
- // their loaded `AkmConfig` directly to `requireAgentProfile` etc.
185
- return undefined;
186
- }