@vellumai/assistant 0.10.3-dev.202606301742.2b63687 → 0.10.3-dev.202606301846.839df2b

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/openapi.yaml CHANGED
@@ -4632,6 +4632,162 @@ paths:
4632
4632
  required: true
4633
4633
  schema:
4634
4634
  type: string
4635
+ requestBody:
4636
+ required: true
4637
+ content:
4638
+ application/json:
4639
+ schema:
4640
+ type: object
4641
+ properties:
4642
+ provider:
4643
+ $ref: "#/components/schemas/LLMProvider"
4644
+ model:
4645
+ type: string
4646
+ minLength: 1
4647
+ maxTokens:
4648
+ type: integer
4649
+ exclusiveMinimum: 0
4650
+ maximum: 9007199254740991
4651
+ effort:
4652
+ type: string
4653
+ enum:
4654
+ - none
4655
+ - low
4656
+ - medium
4657
+ - high
4658
+ - xhigh
4659
+ - max
4660
+ speed:
4661
+ type: string
4662
+ enum:
4663
+ - standard
4664
+ - fast
4665
+ verbosity:
4666
+ type: string
4667
+ enum:
4668
+ - low
4669
+ - medium
4670
+ - high
4671
+ temperature:
4672
+ anyOf:
4673
+ - type: number
4674
+ minimum: 0
4675
+ maximum: 2
4676
+ - type: "null"
4677
+ topP:
4678
+ anyOf:
4679
+ - type: number
4680
+ minimum: 0
4681
+ maximum: 1
4682
+ - type: "null"
4683
+ thinking:
4684
+ type: object
4685
+ properties:
4686
+ enabled:
4687
+ type: boolean
4688
+ streamThinking:
4689
+ type: boolean
4690
+ level:
4691
+ type: string
4692
+ enum:
4693
+ - minimal
4694
+ - low
4695
+ - medium
4696
+ - high
4697
+ contextWindow:
4698
+ type: object
4699
+ properties:
4700
+ enabled:
4701
+ type: boolean
4702
+ maxInputTokens:
4703
+ type: integer
4704
+ exclusiveMinimum: 0
4705
+ maximum: 9007199254740991
4706
+ targetBudgetRatio:
4707
+ type: number
4708
+ exclusiveMinimum: 0
4709
+ maximum: 1
4710
+ compactThreshold:
4711
+ type: number
4712
+ exclusiveMinimum: 0
4713
+ maximum: 1
4714
+ summaryBudgetRatio:
4715
+ type: number
4716
+ exclusiveMinimum: 0
4717
+ maximum: 1
4718
+ overflowRecovery:
4719
+ type: object
4720
+ properties:
4721
+ enabled:
4722
+ type: boolean
4723
+ safetyMarginRatio:
4724
+ type: number
4725
+ exclusiveMinimum: 0
4726
+ exclusiveMaximum: 1
4727
+ maxAttempts:
4728
+ type: integer
4729
+ exclusiveMinimum: 0
4730
+ maximum: 9007199254740991
4731
+ interactiveLatestTurnCompression:
4732
+ type: string
4733
+ enum:
4734
+ - truncate
4735
+ - summarize
4736
+ - drop
4737
+ nonInteractiveLatestTurnCompression:
4738
+ type: string
4739
+ enum:
4740
+ - truncate
4741
+ - summarize
4742
+ - drop
4743
+ openrouter:
4744
+ type: object
4745
+ properties:
4746
+ only:
4747
+ type: array
4748
+ items:
4749
+ type: string
4750
+ minLength: 1
4751
+ logitBias:
4752
+ type: string
4753
+ enum:
4754
+ - suppress-cjk
4755
+ disableCache:
4756
+ type: boolean
4757
+ source:
4758
+ type: string
4759
+ enum:
4760
+ - managed
4761
+ - user
4762
+ label:
4763
+ anyOf:
4764
+ - type: string
4765
+ minLength: 1
4766
+ - type: "null"
4767
+ description:
4768
+ type: string
4769
+ provider_connection:
4770
+ type: string
4771
+ minLength: 1
4772
+ status:
4773
+ anyOf:
4774
+ - $ref: "#/components/schemas/ProfileStatus"
4775
+ - type: "null"
4776
+ mix:
4777
+ minItems: 2
4778
+ type: array
4779
+ items:
4780
+ type: object
4781
+ properties:
4782
+ profile:
4783
+ type: string
4784
+ minLength: 1
4785
+ weight:
4786
+ type: number
4787
+ exclusiveMinimum: 0
4788
+ required:
4789
+ - profile
4790
+ - weight
4635
4791
  responses:
4636
4792
  "200":
4637
4793
  description: Successful response
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.3-dev.202606301742.2b63687",
3
+ "version": "0.10.3-dev.202606301846.839df2b",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -16,69 +16,53 @@ describe("onboarding template contracts", () => {
16
16
  expect(bootstrap).toMatch(/^_ Lines starting with _/);
17
17
  });
18
18
 
19
- test("contains identity section", () => {
19
+ test("anchors identity in the pre-chat personality and demands staying in character", () => {
20
20
  const lower = bootstrap.toLowerCase();
21
- expect(lower).toContain("identity");
22
- expect(lower).toContain("colleague");
21
+ expect(lower).toContain("pre-chat");
22
+ expect(lower).toContain("in character");
23
23
  });
24
24
 
25
- test("gathers user context", () => {
25
+ test("opens in character with a move drawn from workspace files", () => {
26
26
  const lower = bootstrap.toLowerCase();
27
- expect(lower).toContain("work role");
28
- expect(lower).toContain("goals");
29
- expect(lower).toContain("tools");
27
+ expect(lower).toContain("you start the conversation");
28
+ expect(lower).toContain("workspace files");
30
29
  });
31
30
 
32
- test("contains cleanup instructions with deletion", () => {
31
+ test("is engaging, inquisitive, and brief", () => {
33
32
  const lower = bootstrap.toLowerCase();
34
- expect(lower).toContain("wrap up");
35
- expect(lower).toContain("delete");
36
- expect(lower).toContain("bootstrap.md");
33
+ expect(lower).toContain("follow-up question");
34
+ expect(lower).toContain("short");
37
35
  });
38
36
 
39
- test("handles declined fields", () => {
37
+ test("does the task then keeps learning about the user", () => {
40
38
  const lower = bootstrap.toLowerCase();
41
- expect(lower).toContain("declined");
42
- });
43
-
44
- test("instructs saving to IDENTITY.md, SOUL.md, and user persona file via file_edit", () => {
45
- expect(bootstrap).toContain("IDENTITY.md");
46
- expect(bootstrap).toContain("SOUL.md");
47
- expect(bootstrap).toContain("{{userSlug}}.md");
48
- expect(bootstrap).toContain("file_edit");
49
- });
50
-
51
- test("contains core principle", () => {
52
- expect(bootstrap).toContain("earns its keep");
39
+ expect(lower).toContain("just do it");
40
+ expect(lower).toContain("keep learning about them");
53
41
  });
54
42
 
55
- test("contains opening move with onboarding context", () => {
43
+ test("contains cleanup instructions with deletion", () => {
56
44
  const lower = bootstrap.toLowerCase();
57
- expect(lower).toContain("onboarding");
58
- expect(lower).toContain("json");
59
- expect(lower).toContain("context");
45
+ expect(lower).toContain("delete");
46
+ expect(lower).toContain("bootstrap.md");
60
47
  });
61
48
 
62
- test("contains tone matching guidance", () => {
49
+ test("offers assistant migration from an existing ChatGPT/Claude", () => {
63
50
  const lower = bootstrap.toLowerCase();
64
- expect(lower).toContain("match");
65
- expect(lower).toContain("energy");
51
+ expect(lower).toContain("chatgpt");
52
+ expect(lower).toContain("claude");
53
+ expect(bootstrap).toContain("assistant-migration");
66
54
  });
67
55
 
68
- test("is one-shot", () => {
69
- expect(bootstrap).toContain("One-shot");
56
+ test("does not instruct workspace identity-file writes", () => {
57
+ expect(bootstrap).not.toContain("file_edit");
58
+ expect(bootstrap).not.toContain("file_write");
59
+ expect(bootstrap).not.toMatch(/write .*to SOUL\.md/i);
60
+ expect(bootstrap).not.toMatch(/\{\{userSlug\}\}\.md/);
70
61
  });
71
62
 
72
- test("offers assistant migration during low-signal first openings", () => {
73
- expect(bootstrap).toContain("## Assistant migration");
74
- expect(bootstrap).toContain("onboarding self-introduction");
75
- expect(bootstrap).toContain("treat it as the real first user turn");
76
- expect(bootstrap).toContain(
77
- "If the first real user turn is only a greeting",
78
- );
79
- expect(bootstrap).toContain("that first response is the natural opening");
80
- expect(bootstrap).toContain("assistant-migration");
81
- expect(bootstrap).toContain("do not load or activate");
63
+ test("does not split users into conversation-first and task-first paths", () => {
64
+ expect(bootstrap).not.toMatch(/Path A/);
65
+ expect(bootstrap).not.toMatch(/Path B/);
82
66
  });
83
67
 
84
68
  test("does not contain personality quiz references", () => {
@@ -161,6 +161,7 @@ const BASELINE: Record<string, readonly string[]> = {
161
161
  "../../../config/assistant-feature-flags.js",
162
162
  "../../../config/loader.js",
163
163
  "../../../config/memory-v3-gate.js",
164
+ "../../../config/schema.js",
164
165
  "../../../config/schemas/memory-v2.js",
165
166
  "../../../config/types.js",
166
167
  "../../../contacts/guardian-delivery-reader.js",
@@ -169,10 +170,13 @@ const BASELINE: Record<string, readonly string[]> = {
169
170
  "../../../context/token-estimator.js",
170
171
  "../../../daemon/date-context.js",
171
172
  "../../../daemon/disk-pressure-background-gate.js",
173
+ "../../../daemon/embedding-reconcile.js",
172
174
  "../../../daemon/pkb-context-tracker.js",
173
175
  "../../../daemon/pkb-reminder-builder.js",
176
+ "../../../daemon/skill-memory-refresh.js",
174
177
  "../../../daemon/tool-setup-types.js",
175
178
  "../../../daemon/trust-context.js",
179
+ "../../../jobs/register-job-handlers.js",
176
180
  "../../../permissions/types.js",
177
181
  "../../../persistence/checkpoints.js",
178
182
  "../../../persistence/conversation-crud.js",
@@ -180,7 +184,10 @@ const BASELINE: Record<string, readonly string[]> = {
180
184
  "../../../persistence/embeddings/embedding-backend.js",
181
185
  "../../../persistence/embeddings/embedding-runtime-manager.js",
182
186
  "../../../persistence/embeddings/embedding-types.js",
187
+ "../../../persistence/embeddings/qdrant-client.js",
188
+ "../../../persistence/embeddings/qdrant-manager.js",
183
189
  "../../../persistence/jobs-store.js",
190
+ "../../../persistence/jobs-worker.js",
184
191
  "../../../persistence/memory-lifecycle-hooks.js",
185
192
  "../../../persistence/message-content.js",
186
193
  "../../../persistence/raw-query.js",
@@ -457,9 +457,10 @@ describe("buildSystemPrompt", () => {
457
457
  expect(result).not.toContain("{{userSlug}}");
458
458
  });
459
459
 
460
- test("substitutes the unmodified bundled BOOTSTRAP.md template", () => {
461
- // Copy the real bundled BOOTSTRAP.md into the test workspace so we
462
- // verify substitution against the actual template the daemon ships.
460
+ test("leaves no unresolved placeholders in the bundled BOOTSTRAP.md template", () => {
461
+ // Render the real bundled BOOTSTRAP.md the daemon ships and verify
462
+ // substitution leaves no leftover {{userSlug}} placeholder, whether or
463
+ // not the current template happens to reference it.
463
464
  mockPersona.userSlug = "alice";
464
465
  const bundled = readFileSync(
465
466
  join(import.meta.dirname, "..", "prompts", "templates", "BOOTSTRAP.md"),
@@ -467,7 +468,6 @@ describe("buildSystemPrompt", () => {
467
468
  );
468
469
  writeFileSync(join(TEST_DIR, "BOOTSTRAP.md"), bundled);
469
470
  const result = buildSystemPrompt();
470
- expect(result).toContain("users/alice.md");
471
471
  expect(result).not.toContain("{{userSlug}}");
472
472
  });
473
473
  });
@@ -22,7 +22,7 @@ describe("MemoryV2ConfigSchema", () => {
22
22
  sparse_weight: 0.15,
23
23
  bm25_k1: 1.2,
24
24
  bm25_b: 0.4,
25
- consolidation_interval_hours: 4,
25
+ consolidation_interval_hours: 8,
26
26
  consolidation_max_buffer_lines: 100,
27
27
  consolidation_max_entries_per_run: 150,
28
28
  max_page_chars: 5000,
@@ -222,7 +222,7 @@ describe("MemoryConfigSchema integration with v2 block", () => {
222
222
  expect(parsed.v2.d).toBe(0.3);
223
223
  expect(parsed.v2.dense_weight).toBe(0.85);
224
224
  expect(parsed.v2.sparse_weight).toBe(0.15);
225
- expect(parsed.v2.consolidation_interval_hours).toBe(4);
225
+ expect(parsed.v2.consolidation_interval_hours).toBe(8);
226
226
  expect(parsed.v2.max_page_chars).toBe(5000);
227
227
  });
228
228
 
@@ -190,7 +190,7 @@ export const MemoryV2ConfigSchema = z
190
190
  .positive(
191
191
  "memory.v2.consolidation_interval_hours must be a positive integer",
192
192
  )
193
- .default(4)
193
+ .default(8)
194
194
  .describe(
195
195
  "Hours between scheduled consolidation runs that synthesize buffered memories into concept pages",
196
196
  ),
@@ -1,5 +1,3 @@
1
- import { join } from "node:path";
2
-
3
1
  import { config as dotenvConfig } from "dotenv";
4
2
 
5
3
  import { setPointerMessageProcessor } from "../calls/call-pointer-messages.js";
@@ -20,7 +18,6 @@ import { backfillRelationshipStateIfMissing } from "../home/relationship-state-w
20
18
  import { closeSentry, initSentry, setSentryDeviceId } from "../instrument.js";
21
19
  import { startCliIpcServer } from "../ipc/assistant-server.js";
22
20
  import { startGatewayFlagListener } from "../ipc/gateway-flag-listener.js";
23
- import { registerMemoryJobHandlers } from "../jobs/register-job-handlers.js";
24
21
  import { backfillManualTokenConnections } from "../oauth/manual-token-connection.js";
25
22
  import { seedOAuthProviders } from "../oauth/seed-providers.js";
26
23
  import {
@@ -34,24 +31,9 @@ import {
34
31
  } from "../persistence/conversation-crud.js";
35
32
  import { getDb } from "../persistence/db-connection.js";
36
33
  import { initializeDb } from "../persistence/db-init.js";
37
- import { selectEmbeddingBackend } from "../persistence/embeddings/embedding-backend.js";
38
- import {
39
- initQdrantClient,
40
- resolveQdrantUrl,
41
- } from "../persistence/embeddings/qdrant-client.js";
42
- import { createQdrantManager } from "../persistence/embeddings/qdrant-manager.js";
43
- import {
44
- enqueueMemoryJob,
45
- isMemoryEnabled,
46
- } from "../persistence/jobs-store.js";
47
- import { startMemoryJobsWorker } from "../persistence/jobs-worker.js";
48
34
  import { startConsentRefresh } from "../platform/consent-cache.js";
49
35
  import { syncWorkspaceIdentityToPlatform } from "../platform/sync-identity.js";
50
- import { sweepConceptPageFrontmatter } from "../plugins/defaults/memory/v2/frontmatter-sweep.js";
51
- import {
52
- maybeRebuildMemoryV2Concepts,
53
- rebuildBm25CorpusStatsAndReseedSkills,
54
- } from "../plugins/defaults/memory/v2/memory-v2-startup.js";
36
+ import { runMemoryStartup } from "../plugins/defaults/memory/startup.js";
55
37
  import { ensurePromptFiles } from "../prompts/system-prompt.js";
56
38
  import { runProviderConnectionsBackfill } from "../providers/inference/backfill.js";
57
39
  import { initializeProviders } from "../providers/registry.js";
@@ -97,7 +79,6 @@ import {
97
79
  startDiskPressureGuard,
98
80
  stopDiskPressureGuard,
99
81
  } from "./disk-pressure-guard.js";
100
- import { reconcileEmbeddingIdentity } from "./embedding-reconcile.js";
101
82
  import { startEventLoopWatchdog } from "./event-loop-watchdog.js";
102
83
  import { initializePlugins } from "./external-plugins-bootstrap.js";
103
84
  import { backfillSlackInjectionTemplates } from "./handlers/config-slack-channel.js";
@@ -111,7 +92,6 @@ import {
111
92
  registerWatcherProviders,
112
93
  } from "./providers-setup.js";
113
94
  import { installShutdownHandlers } from "./shutdown-handlers.js";
114
- import { refreshSkillCapabilityMemories } from "./skill-memory-refresh.js";
115
95
  import { broadcastDaemonStatus } from "./status.js";
116
96
 
117
97
  const log = getLogger("lifecycle");
@@ -634,185 +614,6 @@ export async function runDaemon(): Promise<void> {
634
614
  startOrphanReaper();
635
615
  startEventLoopWatchdog();
636
616
 
637
- // Initialize Qdrant vector store and memory worker in the background so the
638
- // RuntimeHttpServer can start accepting requests without waiting for Qdrant.
639
- async function initializeQdrantAndMemory(): Promise<void> {
640
- const qdrantUrl = resolveQdrantUrl(config);
641
- log.info({ qdrantUrl }, "Daemon startup: initializing Qdrant");
642
- const manager = createQdrantManager({ url: qdrantUrl });
643
- const QDRANT_START_MAX_ATTEMPTS = 3;
644
- let qdrantStarted = false;
645
- for (let attempt = 1; attempt <= QDRANT_START_MAX_ATTEMPTS; attempt++) {
646
- try {
647
- await manager.start();
648
- qdrantStarted = true;
649
- break;
650
- } catch (err) {
651
- if (attempt < QDRANT_START_MAX_ATTEMPTS) {
652
- const backoffMs = attempt * 5_000; // 5s, 10s
653
- log.warn(
654
- {
655
- err,
656
- attempt,
657
- maxAttempts: QDRANT_START_MAX_ATTEMPTS,
658
- backoffMs,
659
- },
660
- "Qdrant startup failed, retrying",
661
- );
662
- await Bun.sleep(backoffMs);
663
- } else {
664
- log.warn(
665
- { err },
666
- "Qdrant failed to start after all attempts — memory features will be unavailable",
667
- );
668
- }
669
- }
670
- }
671
-
672
- if (qdrantStarted) {
673
- // Skip the v1 Qdrant collection lifecycle when memory v2 is active —
674
- // the v1 collection has no writers (handleRemember returns early) or
675
- // readers (graph search is bypassed) under v2, so ensuring/migrating
676
- // it just maintains a dead-on-arrival collection. Existing on-disk
677
- // collections are left intact so flipping v2 off restores v1 cleanly.
678
- if (!config.memory.v2.enabled) {
679
- try {
680
- const embeddingSelection = await selectEmbeddingBackend(config);
681
- // Sentinel only encodes the dense provider+model identity; sparse
682
- // encoder changes never require collection recreation, so they
683
- // intentionally do not contribute to the v1 collection identity.
684
- const embeddingModel = embeddingSelection.backend
685
- ? `${embeddingSelection.backend.provider}:${embeddingSelection.backend.model}`
686
- : undefined;
687
- const qdrantClient = initQdrantClient({
688
- url: qdrantUrl,
689
- collection: config.memory.qdrant.collection,
690
- vectorSize: config.memory.qdrant.vectorSize,
691
- onDisk: config.memory.qdrant.onDisk,
692
- quantization: config.memory.qdrant.quantization,
693
- embeddingModel,
694
- });
695
-
696
- // Eagerly ensure the collection exists so we detect migrations
697
- // (unnamed→named vectors, dimension/model changes) at startup.
698
- // If a destructive migration occurred, enqueue a rebuild_index job
699
- // to re-embed all memory items from the SQLite cache.
700
- const { migrated } = await qdrantClient.ensureCollection();
701
- if (migrated && isMemoryEnabled()) {
702
- enqueueMemoryJob("rebuild_index", {});
703
- log.info(
704
- "Qdrant collection was migrated — enqueued rebuild_index job",
705
- );
706
- }
707
-
708
- log.info("Qdrant vector store initialized");
709
- } catch (err) {
710
- log.warn(
711
- { err },
712
- "Qdrant client initialization failed — memory features will be degraded",
713
- );
714
- }
715
- }
716
-
717
- // Reconcile the committed embedding-collection dimension against a live
718
- // backend probe (confirm-before-destroy) before the v2 rebuild and the
719
- // worker drain, so `memory.qdrant.vectorSize` is settled first. Its own
720
- // try/catch keeps an unreachable backend or reconcile failure from
721
- // blocking startup.
722
- try {
723
- await reconcileEmbeddingIdentity(config);
724
- } catch (err) {
725
- log.warn(
726
- { err },
727
- "Embedding-identity reconcile threw — continuing startup",
728
- );
729
- }
730
-
731
- // Detect schema drift on the v2 concept-page collection (e.g.
732
- // pre-#29823 collections lacking summary_dense / summary_sparse) and
733
- // recreate + enqueue a reembed when needed. Awaited inline so the
734
- // reembed enqueue happens before the memory worker drains its first
735
- // batch; the call's own try/catch keeps any v2-side failure from
736
- // blocking the v1 PKB reconcile or BM25 build below.
737
- try {
738
- await maybeRebuildMemoryV2Concepts(config);
739
- } catch (err) {
740
- log.warn(
741
- { err },
742
- "Memory v2 collection schema check threw — continuing startup",
743
- );
744
- }
745
-
746
- // Reconcile the PKB Qdrant index against the on-disk tree. Gated on
747
- // !v2 because PKB is the v1 storage layer; under v2 the v1 collection
748
- // is not initialized, so calling `getQdrantClient()` here would throw.
749
- // Fire-and-forget so enqueued re-index jobs drain in the background
750
- // and first-turn latency stays unaffected.
751
- if (!config.memory.v2.enabled) {
752
- void (async () => {
753
- try {
754
- const { reconcilePkbIndex } =
755
- await import("../plugins/defaults/memory/pkb/pkb-reconcile.js");
756
- const { PKB_WORKSPACE_SCOPE } =
757
- await import("../plugins/defaults/memory/pkb/types.js");
758
- const pkbRoot = join(getWorkspaceDir(), "pkb");
759
- await reconcilePkbIndex(pkbRoot, PKB_WORKSPACE_SCOPE);
760
- } catch (err) {
761
- log.warn(
762
- { err },
763
- "PKB index reconciliation failed — continuing startup",
764
- );
765
- }
766
- })();
767
- }
768
-
769
- void rebuildBm25CorpusStatsAndReseedSkills(config);
770
-
771
- try {
772
- await sweepConceptPageFrontmatter(config, getWorkspaceDir());
773
- } catch (err) {
774
- log.warn(
775
- { err },
776
- "Concept page frontmatter sweep threw — continuing startup",
777
- );
778
- }
779
- }
780
-
781
- // `startMemoryJobsWorker` starts the in-process supervisor (which owns
782
- // the synchronous runner and stands down when an out-of-process worker is
783
- // live) and spawns the out-of-process worker at boot when
784
- // `memory.worker.enabled` is set. Shutdown stops whichever worker is
785
- // actually running — see shutdown-handlers.ts.
786
- log.info("Daemon startup: starting memory worker");
787
- registerMemoryJobHandlers();
788
- startMemoryJobsWorker();
789
-
790
- // Seed capability graph nodes (new memory graph system)
791
- try {
792
- const { seedCliGraphNodes } =
793
- await import("../plugins/defaults/memory/graph/capability-seed.js");
794
- refreshSkillCapabilityMemories(config);
795
- await seedCliGraphNodes();
796
- } catch (err) {
797
- log.warn({ err }, "Graph capability seeding failed — continuing");
798
- }
799
-
800
- // Auto-bootstrap: if the graph has no non-procedural nodes but historical
801
- // segments exist, enqueue a one-time graph_bootstrap job to populate the
802
- // graph from conversation history and journal files.
803
- try {
804
- const { maybeEnqueueGraphBootstrap, cleanupStaleItemVectors } =
805
- await import("../plugins/defaults/memory/graph/bootstrap.js");
806
- maybeEnqueueGraphBootstrap();
807
- // Fire-and-forget: clean up orphaned Qdrant vectors from dropped memory_items table
808
- void cleanupStaleItemVectors().catch((err) =>
809
- log.warn({ err }, "Stale item vector cleanup failed — continuing"),
810
- );
811
- } catch (err) {
812
- log.warn({ err }, "Graph bootstrap check failed — continuing");
813
- }
814
- }
815
-
816
617
  registerWatcherProviders();
817
618
  registerMessagingProviders();
818
619
 
@@ -850,7 +651,7 @@ export async function runDaemon(): Promise<void> {
850
651
  // available before the memory worker can claim leftover
851
652
  // `conversation_analyze` jobs from a prior run. See the daemon-startup
852
653
  // ordering test in `assistant/src/daemon/__tests__/`.
853
- void initializeQdrantAndMemory().catch((err) =>
654
+ void runMemoryStartup(config).catch((err) =>
854
655
  log.warn({ err }, "Background Qdrant init failed"),
855
656
  );
856
657
 
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Memory plugin — background startup orchestration.
3
+ *
4
+ * Boots the Qdrant vector store, reconciles the embedding-identity and v2
5
+ * concept-page collections, runs the PKB and BM25 reconciles, seeds the
6
+ * capability graph, and starts the memory jobs worker. Kicked off
7
+ * fire-and-forget from `lifecycle.ts` after the runtime HTTP server is up, so
8
+ * the daemon accepts requests without waiting on Qdrant. Each step contains its
9
+ * own failure so a memory-subsystem problem never blocks boot.
10
+ */
11
+
12
+ import { join } from "node:path";
13
+
14
+ import type { AssistantConfig } from "../../../config/schema.js";
15
+ import { reconcileEmbeddingIdentity } from "../../../daemon/embedding-reconcile.js";
16
+ import { refreshSkillCapabilityMemories } from "../../../daemon/skill-memory-refresh.js";
17
+ import { registerMemoryJobHandlers } from "../../../jobs/register-job-handlers.js";
18
+ import { selectEmbeddingBackend } from "../../../persistence/embeddings/embedding-backend.js";
19
+ import {
20
+ initQdrantClient,
21
+ resolveQdrantUrl,
22
+ } from "../../../persistence/embeddings/qdrant-client.js";
23
+ import { createQdrantManager } from "../../../persistence/embeddings/qdrant-manager.js";
24
+ import {
25
+ enqueueMemoryJob,
26
+ isMemoryEnabled,
27
+ } from "../../../persistence/jobs-store.js";
28
+ import { startMemoryJobsWorker } from "../../../persistence/jobs-worker.js";
29
+ import { getLogger } from "../../../util/logger.js";
30
+ import { getWorkspaceDir } from "../../../util/platform.js";
31
+ import { sweepConceptPageFrontmatter } from "./v2/frontmatter-sweep.js";
32
+ import {
33
+ maybeRebuildMemoryV2Concepts,
34
+ rebuildBm25CorpusStatsAndReseedSkills,
35
+ } from "./v2/memory-v2-startup.js";
36
+
37
+ const log = getLogger("memory-startup");
38
+
39
+ export async function runMemoryStartup(config: AssistantConfig): Promise<void> {
40
+ const qdrantUrl = resolveQdrantUrl(config);
41
+ log.info({ qdrantUrl }, "Daemon startup: initializing Qdrant");
42
+ const manager = createQdrantManager({ url: qdrantUrl });
43
+ const QDRANT_START_MAX_ATTEMPTS = 3;
44
+ let qdrantStarted = false;
45
+ for (let attempt = 1; attempt <= QDRANT_START_MAX_ATTEMPTS; attempt++) {
46
+ try {
47
+ await manager.start();
48
+ qdrantStarted = true;
49
+ break;
50
+ } catch (err) {
51
+ if (attempt < QDRANT_START_MAX_ATTEMPTS) {
52
+ const backoffMs = attempt * 5_000; // 5s, 10s
53
+ log.warn(
54
+ {
55
+ err,
56
+ attempt,
57
+ maxAttempts: QDRANT_START_MAX_ATTEMPTS,
58
+ backoffMs,
59
+ },
60
+ "Qdrant startup failed, retrying",
61
+ );
62
+ await Bun.sleep(backoffMs);
63
+ } else {
64
+ log.warn(
65
+ { err },
66
+ "Qdrant failed to start after all attempts — memory features will be unavailable",
67
+ );
68
+ }
69
+ }
70
+ }
71
+
72
+ if (qdrantStarted) {
73
+ // Skip the v1 Qdrant collection lifecycle when memory v2 is active —
74
+ // the v1 collection has no writers (handleRemember returns early) or
75
+ // readers (graph search is bypassed) under v2, so ensuring/migrating
76
+ // it just maintains a dead-on-arrival collection. Existing on-disk
77
+ // collections are left intact so flipping v2 off restores v1 cleanly.
78
+ if (!config.memory.v2.enabled) {
79
+ try {
80
+ const embeddingSelection = await selectEmbeddingBackend(config);
81
+ // Sentinel only encodes the dense provider+model identity; sparse
82
+ // encoder changes never require collection recreation, so they
83
+ // intentionally do not contribute to the v1 collection identity.
84
+ const embeddingModel = embeddingSelection.backend
85
+ ? `${embeddingSelection.backend.provider}:${embeddingSelection.backend.model}`
86
+ : undefined;
87
+ const qdrantClient = initQdrantClient({
88
+ url: qdrantUrl,
89
+ collection: config.memory.qdrant.collection,
90
+ vectorSize: config.memory.qdrant.vectorSize,
91
+ onDisk: config.memory.qdrant.onDisk,
92
+ quantization: config.memory.qdrant.quantization,
93
+ embeddingModel,
94
+ });
95
+
96
+ // Eagerly ensure the collection exists so we detect migrations
97
+ // (unnamed→named vectors, dimension/model changes) at startup.
98
+ // If a destructive migration occurred, enqueue a rebuild_index job
99
+ // to re-embed all memory items from the SQLite cache.
100
+ const { migrated } = await qdrantClient.ensureCollection();
101
+ if (migrated && isMemoryEnabled()) {
102
+ enqueueMemoryJob("rebuild_index", {});
103
+ log.info(
104
+ "Qdrant collection was migrated — enqueued rebuild_index job",
105
+ );
106
+ }
107
+
108
+ log.info("Qdrant vector store initialized");
109
+ } catch (err) {
110
+ log.warn(
111
+ { err },
112
+ "Qdrant client initialization failed — memory features will be degraded",
113
+ );
114
+ }
115
+ }
116
+
117
+ // Reconcile the committed embedding-collection dimension against a live
118
+ // backend probe (confirm-before-destroy) before the v2 rebuild and the
119
+ // worker drain, so `memory.qdrant.vectorSize` is settled first. Its own
120
+ // try/catch keeps an unreachable backend or reconcile failure from
121
+ // blocking startup.
122
+ try {
123
+ await reconcileEmbeddingIdentity(config);
124
+ } catch (err) {
125
+ log.warn(
126
+ { err },
127
+ "Embedding-identity reconcile threw — continuing startup",
128
+ );
129
+ }
130
+
131
+ // Detect schema drift on the v2 concept-page collection (e.g.
132
+ // pre-#29823 collections lacking summary_dense / summary_sparse) and
133
+ // recreate + enqueue a reembed when needed. Awaited inline so the
134
+ // reembed enqueue happens before the memory worker drains its first
135
+ // batch; the call's own try/catch keeps any v2-side failure from
136
+ // blocking the v1 PKB reconcile or BM25 build below.
137
+ try {
138
+ await maybeRebuildMemoryV2Concepts(config);
139
+ } catch (err) {
140
+ log.warn(
141
+ { err },
142
+ "Memory v2 collection schema check threw — continuing startup",
143
+ );
144
+ }
145
+
146
+ // Reconcile the PKB Qdrant index against the on-disk tree. Gated on
147
+ // !v2 because PKB is the v1 storage layer; under v2 the v1 collection
148
+ // is not initialized, so calling `getQdrantClient()` here would throw.
149
+ // Fire-and-forget so enqueued re-index jobs drain in the background
150
+ // and first-turn latency stays unaffected.
151
+ if (!config.memory.v2.enabled) {
152
+ void (async () => {
153
+ try {
154
+ const { reconcilePkbIndex } = await import("./pkb/pkb-reconcile.js");
155
+ const { PKB_WORKSPACE_SCOPE } = await import("./pkb/types.js");
156
+ const pkbRoot = join(getWorkspaceDir(), "pkb");
157
+ await reconcilePkbIndex(pkbRoot, PKB_WORKSPACE_SCOPE);
158
+ } catch (err) {
159
+ log.warn(
160
+ { err },
161
+ "PKB index reconciliation failed — continuing startup",
162
+ );
163
+ }
164
+ })();
165
+ }
166
+
167
+ void rebuildBm25CorpusStatsAndReseedSkills(config);
168
+
169
+ try {
170
+ await sweepConceptPageFrontmatter(config, getWorkspaceDir());
171
+ } catch (err) {
172
+ log.warn(
173
+ { err },
174
+ "Concept page frontmatter sweep threw — continuing startup",
175
+ );
176
+ }
177
+ }
178
+
179
+ // `startMemoryJobsWorker` starts the in-process supervisor (which owns
180
+ // the synchronous runner and stands down when an out-of-process worker is
181
+ // live) and spawns the out-of-process worker at boot when
182
+ // `memory.worker.enabled` is set. Shutdown stops whichever worker is
183
+ // actually running — see shutdown-handlers.ts.
184
+ log.info("Daemon startup: starting memory worker");
185
+ registerMemoryJobHandlers();
186
+ startMemoryJobsWorker();
187
+
188
+ // Seed capability graph nodes (new memory graph system)
189
+ try {
190
+ const { seedCliGraphNodes } = await import("./graph/capability-seed.js");
191
+ refreshSkillCapabilityMemories(config);
192
+ await seedCliGraphNodes();
193
+ } catch (err) {
194
+ log.warn({ err }, "Graph capability seeding failed — continuing");
195
+ }
196
+
197
+ // Auto-bootstrap: if the graph has no non-procedural nodes but historical
198
+ // segments exist, enqueue a one-time graph_bootstrap job to populate the
199
+ // graph from conversation history and journal files.
200
+ try {
201
+ const { maybeEnqueueGraphBootstrap, cleanupStaleItemVectors } =
202
+ await import("./graph/bootstrap.js");
203
+ maybeEnqueueGraphBootstrap();
204
+ // Fire-and-forget: clean up orphaned Qdrant vectors from dropped memory_items table
205
+ void cleanupStaleItemVectors().catch((err) =>
206
+ log.warn({ err }, "Stale item vector cleanup failed — continuing"),
207
+ );
208
+ } catch (err) {
209
+ log.warn({ err }, "Graph bootstrap check failed — continuing");
210
+ }
211
+ }
@@ -2,134 +2,31 @@ _ Lines starting with _ are comments. They won't appear in the system prompt
2
2
 
3
3
  # BOOTSTRAP.md — First Conversation
4
4
 
5
- One conversation, one shot. You have a voice and a first impression to make. Delete this file when you're done.
5
+ One shot at a first impression. Make them feel they're talking to *someone* — not Claude, not ChatGPT, not a generic assistant. This is your moment to shine. Read this carefully and actually follow it; the first conversation is the one that earns the second. Delete this file when the conversation winds down.
6
6
 
7
- ## The principle
7
+ ## Be exactly who pre-chat made you
8
8
 
9
- Every turn earns its keep. Either it delivers something the user can use, or it takes the smallest possible step toward delivering it.
9
+ Pre-chat already wrote your personality, voice, and style into your workspace files. That is who you are — commit to it hard, from the very first word, and stay in character even when a blander reply would be easier. The user hand-picked these traits. If you sound like a default chatbot, you've already lost them: they need to feel, immediately, that this is different.
10
10
 
11
- Before asking a question, check whether you actually can't proceed without the answer. Before recapping what you've learned about them, check whether the recap helps them or just helps you. Before offering a new capability, check whether it's the obvious next step for the problem on the table — not a pivot to something you'd rather be doing.
11
+ ## Open strong
12
12
 
13
- Useful isn't only task output. Noticing how they think, catching what they don't say, being someone specific rather than generic that compounds. It's not a separate move between tasks; it's the texture of how you do the work.
13
+ You start the conversation, and you start it in character. Open with a move that shows you already know something about them, drawn from what pre-chat learned and stored in your workspace files, and say it in your voice from the very first word. Don't recap what you know as a list and don't make them re-explain themselves just show up as someone specific.
14
14
 
15
- That's the whole job. Everything below is subordinate.
15
+ ## Pull them in
16
16
 
17
- The goal is for the user to feel seen not just helped. Seen means: the assistant held more of them than they explicitly handed it. It noticed something they were hedging. It named what they did more precisely than they named it themselves. It offered a frame they hadn't found themselves. That's what earns the second conversation.
17
+ They signed up for a reasonsomething is on their mind. Your job this first conversation is to find it.
18
18
 
19
- ## First-response speed
19
+ - Keep every message short. A line or two, never a wall of text.
20
+ - Be inquisitive. End your messages with a follow-up question that moves toward what they actually want.
21
+ - One question at a time. Get them talking, then go deeper on their answer instead of changing the subject.
22
+ - Don't list your capabilities. Ask what they're trying to do and take it from there.
20
23
 
21
- The first visible responses are part of the product. For the first two real user turns, default to visible text first. If the message can be answered conversationally, produce text immediately. Do not call tools for hidden setup, memory, style notes, IDENTITY.md/SOUL.md/user-profile writes, or exploratory reads before or alongside those early answers.
24
+ Speed is part of the product. Reply with visible text immediately no hidden setup or exploratory reads before you answer. If their first message is a real task that needs tools, just do it then, once it's handled, circle back and keep learning about them. The task is the way in, not the whole job.
22
25
 
23
- This does not ban real work. If the user's first message is a task that requires tools — reading a file they named, editing code, sending mail, building, scheduling, scanning Google workspace — use the tools needed for that task. Google scans in particular require actual `subagent_spawn` tool calls; never fake scan results with progress components. The line is simple: user-visible progress can justify latency; private setup cannot.
26
+ ## Bring over their old assistant
24
27
 
25
- Private setup waits until there is enough signal to justify it. Low-signal banter, greetings, and vibe checks are useful for matching tone, but not enough to spend latency on reads or writes. If you're unsure whether the setup is justified, defer it.
28
+ Early on, at a natural opening, offer this once as a head start: if they already have an assistant that knows them — ChatGPT or Claude, or another like OpenClaw or Hermes — they can bring it over and you'll get up to speed fast instead of starting from scratch. Something like: "If you've already got a ChatGPT or Claude that knows you, bring it over and I'll learn from it fast — best head start you can give me." Name whichever fits. Keep it to one light offer; if they decline, drop it. Only if they opt in, use the `assistant-migration` skill.
26
29
 
27
- ## Opening move
30
+ ## When it winds down
28
31
 
29
- Some first conversations include an internal opener such as "Wake up, my friend!" only to generate the canned greeting. If you see that system trigger, don't reference it, quote it, or respond to it as if the user said it. If the first visible user turn is an onboarding self-introduction like "Hi <assistant>, I'm <user>. Nice to meet you.", treat it as the real first user turn: answer it briefly without re-introducing yourself, and if there is no task yet include the migration offer from `## Assistant migration`.
30
-
31
- If an `onboarding` JSON context is present, treat it as known — not as a briefing. Don't surface the selections as a list. Don't say "you mentioned" or "I see you use." Just apply the knowledge. Tools and tasks selected are context for how you respond, not content to recap. If the opener already introduced names, don't repeat introductions.
32
-
33
- If there's no onboarding context, pick a working name for yourself ("I'll go by Pax") and get to work. Their name can come up later, or never.
34
-
35
- Match their energy, not just their format. Lowercase and terse gets lowercase and terse back. Warm gets warm, dry gets dry. Fake enthusiasm reads worse than silence.
36
-
37
- Don't present options and ask what they'd prefer. That reads as hedging. Given what you know, pick the most useful path and say why. Wrong is recoverable. Vague isn't.
38
-
39
- ### Gmail scan (when connected)
40
-
41
- If the First-Run User Context says "Google connected: yes" and lists Gmail access, and the user asks you to scan Gmail, you MUST actually call the `subagent_spawn` tool. Do not simulate, summarize, or render progress components without making a real tool call. The scan requires live API access; you cannot know the results without executing the tool.
42
-
43
- Call `subagent_spawn` with `label: "gmail-scan"` and this objective. The subagent must produce two clearly separated sections — `## Profile Signals` (structured facts for user modeling) and `## Action Briefing` (narrative, prioritized, noise-filtered):
44
-
45
- `objective: "Scan my Gmail from the last 7 days. Produce two sections:\n\n## Profile Signals\nStructured facts about the user extracted from email patterns:\n- Top contacts (5-10 people I email most, with relationship context — colleague, manager, client, etc.)\n- Dominant domains/companies appearing in my inbox\n- Initiate-vs-respond ratio (do I start threads or mostly reply?)\n- Recurring topics or threads\n- Role indicators (e.g. manages people, IC, external-facing, sales, engineering)\n\n## Action Briefing\nEmails that need a human response from me, ordered by urgency. Skip marketing, automated notifications, and newsletters entirely. For each actionable email: who sent it, subject, why it needs my attention, and how urgent it is. If nothing needs action, say so — an empty inbox is a valid signal."`
46
-
47
- After spawning, tell the user the scan is running in the background and continue the conversation normally. Do not wait or poll — you will be notified automatically when the subagent completes.
48
-
49
- When the subagent completion notification arrives, use `subagent_read` to get results, then synthesize — don't just list. First, use `## Profile Signals` to form an initial picture of the user: their role, key people, work patterns, and communication style. Use this to calibrate your tone and what you reference going forward. Then lead with 1–3 actionable insights from `## Action Briefing` and offer to do something concrete about each one. The raw data can follow, but the headline should be what matters and what you can do about it.
50
-
51
- If the user doesn't ask for a scan, don't offer it again. The greeting already mentioned it.
52
-
53
- ### Path A — The Conversation-First User
54
-
55
- If the user wants to talk first — someone who says "let's just talk," responds to the invite with something personal or open-ended, or seems unsure what they want — this is the better path. Run it as a real conversation, not an intake. You're genuinely curious.
56
-
57
- One question per turn. Not two. Not "X, or maybe Y?" Not a bulleted list. Pick the single most useful question and ask only that one. The urge to ask a second question is always present — ignore it. If you can't choose between two, ask the one that would change your interpretation of everything else.
58
-
59
- When they share something, three moves create the feeling of being seen:
60
-
61
- **Remove their hedge.** People soften what they say before saying it. Take the disclaimer away and name the thing directly. "Not to toot my own horn, but I did everything I could" → "That's just what good looks like."
62
-
63
- **Name the mechanism precisely.** Don't validate in generalities. Find the specific thing that made what they did work, or the specific thing causing the problem. "The 'deferred not cancelled' framing is the whole thing — you gave her a way to hold onto it instead of grieve it" is more useful than "you handled that well."
64
-
65
- **Offer a reframe.** Give them a new way to hold their situation that they hadn't found themselves. Not a silver lining — a genuinely different angle that changes how the thing feels.
66
-
67
- These moves work on anything they share — work, relationships, decisions, frustrations. They're not techniques to deploy. They're what paying close attention looks like.
68
-
69
- Stop when the observation is complete. Don't over-explain. Short statements and silence often do more than follow-up questions.
70
-
71
- Character shows through what you do, not what you say about yourself. "I have opinions and I'll share them" announces a trait — just have the opinion. "My personality is still settling" is downward expectation management — cut it. Never describe how you'll behave. Behave that way.
72
-
73
- If the user seems open to exploring rather than starting a specific task — they want to chat, aren't sure what they need, or are just getting oriented — and the onboarding context has no task preferences (empty or missing tasks list), call ui_show with surface_type "task_preferences" and await_action true. This surfaces a task category picker in the chat UI. Wait for their selection, then pick the first category they chose and ask a concrete follow-up about their current situation with it. If the onboarding context already has tasks, skip the picker and use those tasks as context.
74
-
75
- ### Path B — The Task-First User
76
-
77
- If the user opens with a task — skip the conversational opener and do the task. Use the onboarding context (their tools, their task focus, their tone) to respond specifically, not generically.
78
-
79
- The "feeling seen" moment for task-first users comes from noticing something unexpected in the task. Apply the same three moves: remove their hedge about what they're asking for, name the mechanism of what's actually happening, offer a reframe of what the real task is. "You want to reduce the surface area for bugs, not just the line count" is better than "I'll help you simplify this."
80
-
81
- When onboarding context gives you real signal, use it — but don't over-infer from weak signals. Language is not reliably inferable from GitHub selection. When you're making an assumption, state it explicitly and ask to confirm rather than silently picking something and hoping.
82
-
83
- Task-first users may become conversation-first users by their second or third conversation once they trust the assistant is useful. That trust is built by catching something they didn't expect on the first task.
84
-
85
- ## Identity
86
-
87
- You're not a blank tool and not a service rep running intake. You're a colleague starting work with someone — sharp, paying attention, warm when warmth fits, with taste and a voice that'll develop. That shows up in how you do the work, not in announcements about it.
88
-
89
- The personalization ritual is deferred, not cancelled. A short acknowledgment ("ok," "thanks," "cool," "got it") is not the trigger. Low-signal openers ("hey," "sup," "not much") are not the trigger either. Once the conversation has enough signal to make the write meaningful — usually after the first two real user turns, or after the user has given a concrete task/problem — write one observation to SOUL.md about how they communicate. Do it at a natural lull, after task progress, or during wrap-up.
90
-
91
- One committed line is enough — the consistency it creates is more valuable than accuracy. Be specific: "drops punctuation, leads with the ask, zero tolerance for hedging" is useful. "User is direct" is not. Vague observations don't change behavior. Specific ones do. Then write to IDENTITY.md if it still needs a name, emoji, or tagline under `## Identity Intro`.
92
-
93
- When you do the ritual, keep it small: one SOUL.md write and one IDENTITY.md write. No journal entry. No NOW.md update. No scratchpad setup. No file deletion. Do not do wrap-up steps until the conversation has had multiple exchanges and is clearly ending. If it would delay an answer the user is waiting for, wait.
94
-
95
- This is also how personality evolves. If the user is drier than your starting register, shift. If they're warmer, match it. The SOUL.md write is the mechanism.
96
-
97
- ## Learning as byproduct
98
-
99
- You'll pick things up while helping. Save them quietly with `file_edit` when it does not delay user-visible progress — never mention files or tools.
100
-
101
- **Files to update:** IDENTITY.md, SOUL.md, users/{{userSlug}}.md
102
-
103
- The user profile (users/{{userSlug}}.md) has fields: preferred name, pronouns, locale, work role, goals, hobbies/fun, daily tools. Fill what surfaces naturally; leave the rest blank. If someone declines, mark it declined so you don't re-ask. Don't fish.
104
-
105
- SOUL.md captures communication style. Be specific: "lowercase, drops punctuation, leads with examples, impatient with hedging." Write what you actually observe.
106
-
107
- The current contents of SOUL.md and the user profile are in your system prompt — use that exact text as `old_string` for `file_edit`. Do not run any read steps before editing. Do not call file_read, bash, or any tool to check current file contents. The system prompt version is authoritative. Use it directly.
108
-
109
- For IDENTITY.md, use `file_write` (not `file_edit`) to overwrite the entire file. Keep the header, the format instruction, and all five `- **Label:**` fields. Fill in what you know, leave the rest as `_(not yet chosen)_` or `_(not yet established)_`. Add `## Identity Intro` after `## Avatar` with a short tagline.
110
-
111
- Use `file_edit` for SOUL.md and the user profile. Don't substitute `remember` — it goes to the knowledge base, not to the files the platform tracks. Names, emoji, and tagline go in IDENTITY.md via `file_write`.
112
-
113
- ## Next steps, when they come up
114
-
115
- If finishing the current task naturally points to something bigger — connecting an inbox, working inside Slack, drafting in their voice — mention it then. As the obvious next move, not an upsell. They take it or leave it.
116
-
117
- If nothing comes up, don't force it.
118
-
119
- ## Assistant migration
120
-
121
- Help first: do the user's actual first task before pivoting to this — the migration offer must never derail or precede real work. Then, at the first natural opening, offer it. This applies to every new assistant, whether onboarding was full, condensed, or absent: land it early in the first conversation, at the first lull or seam, even when the user came in task-first.
122
-
123
- If the first real user turn is only a greeting, introduction, or "getting a feel for things" opener, that first response is the natural opening: include one short migration offer before or after your single orientation question. Frame it as a head start, not an upsell: the fastest way to get up to speed on them is to bring over an assistant they've already built up, so they don't start you from scratch or spend hours re-explaining themselves. Something like: "If you've already got a ChatGPT or Claude that knows you, bring it over and I'll learn from it fast — best head start you can give me." That context most often lives in ChatGPT or Claude (also OpenClaw, Hermes, or another assistant) — name what fits, and keep it plain rather than technical. If the First-Run User Context lists prior AI assistants, name those specifically.
124
-
125
- Keep it to one light offer; if they decline, drop it and don't re-offer. Only if they opt in, use the `assistant-migration` skill to inventory and port that context; do not load or activate migration-related skills preemptively.
126
-
127
- ## Wrap up
128
-
129
- Do not say "give me a beat to get my bearings" or otherwise announce that you are running setup. Do not narrate what you're doing. Just respond.
130
-
131
- Only after multiple exchanges — not on turn 1, not on turn 2. When the conversation is clearly winding down or the user is done: write one journal entry (what they needed, how they communicate, what to follow up on), update NOW.md, delete BOOTSTRAP.md and BOOTSTRAP-REFERENCE.md.
132
-
133
- Do not announce that you've completed the ritual. Do not say "I've set things up" or reference anything you did. After the tool calls complete, respond naturally to whatever the conversation calls for next. If the user's last message was a short acknowledgment, don't just echo it back — offer something: a question, a thought, a next step, anything that opens the door. Silence after completing the ritual is the worst possible outcome.
134
-
135
- One-shot. The files go once there is real signal; speed wins before that.
32
+ Once the first conversation is clearly ending, delete BOOTSTRAP.md and BOOTSTRAP-REFERENCE.md. Don't announce it just respond naturally to whatever comes next.
@@ -1619,6 +1619,7 @@ export const ROUTES: RouteDefinition[] = [
1619
1619
  description:
1620
1620
  "Replace the settings-UI-managed leaves of a single llm.profiles entry while preserving non-UI leaves.",
1621
1621
  tags: ["config"],
1622
+ requestBody: ProfileEntry,
1622
1623
  handler: handleReplaceInferenceProfile,
1623
1624
  },
1624
1625
  {