iranti 0.2.44 → 0.2.46

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.
@@ -29,6 +29,7 @@ const backends_1 = require("../src/library/backends");
29
29
  const runtimeLifecycle_1 = require("../src/lib/runtimeLifecycle");
30
30
  const sdk_1 = require("../src/sdk");
31
31
  const queries_1 = require("../src/library/queries");
32
+ const autoRemember_1 = require("../src/lib/autoRemember");
32
33
  class CliError extends Error {
33
34
  constructor(code, message, hints = [], details) {
34
35
  super(message);
@@ -766,12 +767,28 @@ async function inspectInstanceConfig(root, name) {
766
767
  let metaReadable = false;
767
768
  let envReadable = false;
768
769
  const ownershipIssues = [];
770
+ let rewrittenMeta = false;
769
771
  if (metaPresent) {
770
772
  try {
771
773
  const raw = await promises_1.default.readFile(metaFile, 'utf8');
772
774
  const parsed = JSON.parse(raw);
773
775
  metaReadable = typeof parsed.name === 'string' && parsed.name.trim().length > 0;
774
776
  if (metaReadable) {
777
+ const shouldRewriteOwnership = parsed.name?.trim() === name
778
+ && ((typeof parsed.instanceDir === 'string' && path_1.default.resolve(parsed.instanceDir) !== path_1.default.resolve(instanceDir))
779
+ || (typeof parsed.envFile === 'string' && path_1.default.resolve(parsed.envFile) !== path_1.default.resolve(envFile)));
780
+ if (shouldRewriteOwnership) {
781
+ const nextMeta = {
782
+ ...parsed,
783
+ name,
784
+ instanceDir,
785
+ envFile,
786
+ };
787
+ await writeText(metaFile, `${JSON.stringify(nextMeta, null, 2)}\n`);
788
+ rewrittenMeta = true;
789
+ parsed.instanceDir = instanceDir;
790
+ parsed.envFile = envFile;
791
+ }
775
792
  if (parsed.name?.trim() !== name) {
776
793
  ownershipIssues.push(`instance.json name is ${parsed.name}`);
777
794
  }
@@ -781,6 +798,10 @@ async function inspectInstanceConfig(root, name) {
781
798
  if (typeof parsed.envFile === 'string' && path_1.default.resolve(parsed.envFile) !== path_1.default.resolve(envFile)) {
782
799
  ownershipIssues.push(`instance.json envFile points to ${parsed.envFile}`);
783
800
  }
801
+ const databaseIntentValidation = parseInstanceDatabaseIntent(parsed.databaseIntent);
802
+ if (databaseIntentValidation.errors.length > 0) {
803
+ ownershipIssues.push(`instance.json databaseIntent invalid: ${databaseIntentValidation.errors.join(', ')}`);
804
+ }
784
805
  const dependencyValidation = (0, runtimeDependencies_1.parseInstanceDependencies)(parsed.dependencies);
785
806
  if (dependencyValidation.errors.length > 0) {
786
807
  ownershipIssues.push(`instance.json dependencies invalid: ${dependencyValidation.errors.join(', ')}`);
@@ -808,7 +829,9 @@ async function inspectInstanceConfig(root, name) {
808
829
  }
809
830
  else if (metaPresent && envPresent && metaReadable && envReadable) {
810
831
  classification = 'complete';
811
- detail = 'instance metadata and env are present';
832
+ detail = rewrittenMeta
833
+ ? 'instance metadata was repaired and env is present'
834
+ : 'instance metadata and env are present';
812
835
  }
813
836
  else if ((metaPresent && !metaReadable) || (envPresent && !envReadable)) {
814
837
  classification = 'invalid';
@@ -1142,6 +1165,160 @@ function isLocalPostgresHost(hostname) {
1142
1165
  const normalized = hostname.trim().toLowerCase();
1143
1166
  return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1';
1144
1167
  }
1168
+ function parseDatabaseIntentStrategy(raw) {
1169
+ const normalized = raw?.trim().toLowerCase() ?? '';
1170
+ if (!normalized)
1171
+ return null;
1172
+ if (normalized === 'dedicated' || normalized === 'dedicated-local')
1173
+ return 'dedicated-local';
1174
+ if (normalized === 'shared' || normalized === 'shared-local')
1175
+ return 'shared-local';
1176
+ if (normalized === 'external' || normalized === 'managed' || normalized === 'external-existing')
1177
+ return 'external-existing';
1178
+ return null;
1179
+ }
1180
+ function parseDatabaseIntentStrategyFlag(raw, label) {
1181
+ const trimmed = raw?.trim() ?? '';
1182
+ if (!trimmed)
1183
+ return null;
1184
+ const parsed = parseDatabaseIntentStrategy(trimmed);
1185
+ if (!parsed) {
1186
+ throw new Error(`Invalid ${label} '${raw}'. Use dedicated, shared, or external.`);
1187
+ }
1188
+ return parsed;
1189
+ }
1190
+ function databaseIntentPromptValue(strategy) {
1191
+ if (strategy === 'dedicated-local')
1192
+ return 'dedicated';
1193
+ if (strategy === 'shared-local')
1194
+ return 'shared';
1195
+ return 'external';
1196
+ }
1197
+ function describeDatabaseIntentStrategy(strategy) {
1198
+ switch (strategy) {
1199
+ case 'dedicated-local':
1200
+ return 'dedicated local database';
1201
+ case 'shared-local':
1202
+ return 'shared local database';
1203
+ case 'external-existing':
1204
+ default:
1205
+ return 'external existing database';
1206
+ }
1207
+ }
1208
+ function inferDatabaseProvisioning(databaseUrl, dependencies = []) {
1209
+ if (dependencies.some((dependency) => dependency.kind === 'docker-container')) {
1210
+ return 'docker';
1211
+ }
1212
+ const parsed = parsePostgresConnectionString(databaseUrl);
1213
+ return isLocalPostgresHost(parsed.hostname) ? 'local' : 'managed';
1214
+ }
1215
+ function inferDatabaseIntentStrategy(options) {
1216
+ const databaseName = postgresDatabaseName(options.databaseUrl);
1217
+ if (options.databaseMode === 'managed') {
1218
+ return 'external-existing';
1219
+ }
1220
+ return databaseName === `iranti_${options.instanceName}`
1221
+ ? 'dedicated-local'
1222
+ : 'shared-local';
1223
+ }
1224
+ function buildInstanceDatabaseIntent(options) {
1225
+ const parsed = parsePostgresConnectionString(options.databaseUrl);
1226
+ const strategy = options.strategy ?? inferDatabaseIntentStrategy({
1227
+ instanceName: options.instanceName,
1228
+ databaseMode: options.databaseMode,
1229
+ databaseUrl: options.databaseUrl,
1230
+ });
1231
+ const port = Number.parseInt(parsed.port || '5432', 10);
1232
+ return {
1233
+ strategy,
1234
+ provisioning: options.databaseMode,
1235
+ host: parsed.hostname,
1236
+ ...(Number.isFinite(port) && port > 0 ? { port } : {}),
1237
+ database: postgresDatabaseName(options.databaseUrl),
1238
+ ...(options.dockerContainerName ? { dockerContainerName: options.dockerContainerName } : {}),
1239
+ };
1240
+ }
1241
+ function parseInstanceDatabaseIntent(raw) {
1242
+ if (raw === undefined || raw === null) {
1243
+ return { intent: null, errors: [] };
1244
+ }
1245
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1246
+ return { intent: null, errors: ['databaseIntent must be an object'] };
1247
+ }
1248
+ const record = raw;
1249
+ const strategy = parseDatabaseIntentStrategy(typeof record.strategy === 'string' ? record.strategy : undefined);
1250
+ const provisioningRaw = typeof record.provisioning === 'string' ? record.provisioning.trim().toLowerCase() : '';
1251
+ const provisioning = provisioningRaw === 'local' || provisioningRaw === 'managed' || provisioningRaw === 'docker'
1252
+ ? provisioningRaw
1253
+ : null;
1254
+ const host = typeof record.host === 'string' ? record.host.trim() : '';
1255
+ const database = typeof record.database === 'string' ? record.database.trim() : '';
1256
+ const portRaw = typeof record.port === 'number'
1257
+ ? record.port
1258
+ : typeof record.port === 'string'
1259
+ ? Number.parseInt(record.port, 10)
1260
+ : undefined;
1261
+ const dockerContainerName = typeof record.dockerContainerName === 'string'
1262
+ ? record.dockerContainerName.trim()
1263
+ : undefined;
1264
+ const errors = [];
1265
+ if (!strategy) {
1266
+ errors.push('databaseIntent.strategy must be dedicated-local, shared-local, or external-existing');
1267
+ }
1268
+ if (!provisioning) {
1269
+ errors.push('databaseIntent.provisioning must be local, managed, or docker');
1270
+ }
1271
+ if (!host) {
1272
+ errors.push('databaseIntent.host is required');
1273
+ }
1274
+ if (!database) {
1275
+ errors.push('databaseIntent.database is required');
1276
+ }
1277
+ if (portRaw !== undefined && (!Number.isFinite(portRaw) || portRaw <= 0)) {
1278
+ errors.push('databaseIntent.port must be a positive integer');
1279
+ }
1280
+ if (errors.length > 0 || !strategy || !provisioning || !host || !database) {
1281
+ return { intent: null, errors };
1282
+ }
1283
+ return {
1284
+ intent: {
1285
+ strategy,
1286
+ provisioning,
1287
+ host,
1288
+ ...(portRaw !== undefined ? { port: portRaw } : {}),
1289
+ database,
1290
+ ...(dockerContainerName ? { dockerContainerName } : {}),
1291
+ },
1292
+ errors: [],
1293
+ };
1294
+ }
1295
+ function resolveInstanceDatabaseIntent(options) {
1296
+ const parsedMeta = parseInstanceDatabaseIntent(options.meta?.databaseIntent);
1297
+ if (parsedMeta.intent) {
1298
+ return { intent: parsedMeta.intent, source: 'meta', errors: parsedMeta.errors };
1299
+ }
1300
+ const dbUrl = options.env.DATABASE_URL?.trim();
1301
+ if (!dbUrl || detectPlaceholder(dbUrl)) {
1302
+ return { intent: null, source: 'none', errors: parsedMeta.errors };
1303
+ }
1304
+ const dockerDependency = options.dependencies.find((dependency) => dependency.kind === 'docker-container');
1305
+ return {
1306
+ intent: buildInstanceDatabaseIntent({
1307
+ instanceName: options.instanceName,
1308
+ databaseMode: inferDatabaseProvisioning(dbUrl, options.dependencies),
1309
+ databaseUrl: dbUrl,
1310
+ dockerContainerName: dockerDependency?.name,
1311
+ }),
1312
+ source: 'inferred',
1313
+ errors: parsedMeta.errors,
1314
+ };
1315
+ }
1316
+ function describeInstanceDatabaseIntent(intent, source = 'meta') {
1317
+ const target = `${intent.host}${intent.port ? `:${intent.port}` : ''}/${intent.database}`;
1318
+ const sourceSuffix = source === 'inferred' ? ' (inferred)' : '';
1319
+ const dockerSuffix = intent.dockerContainerName ? `, container ${intent.dockerContainerName}` : '';
1320
+ return `${describeDatabaseIntentStrategy(intent.strategy)} via ${intent.provisioning} -> ${target}${dockerSuffix}${sourceSuffix}`;
1321
+ }
1145
1322
  function sanitizeIdentifier(input, fallback) {
1146
1323
  const value = input.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
1147
1324
  if (!value && input.trim()) {
@@ -1286,6 +1463,7 @@ async function ensureInstanceConfigured(root, name, config) {
1286
1463
  port: config.port,
1287
1464
  envFile,
1288
1465
  instanceDir,
1466
+ databaseIntent: config.databaseIntent,
1289
1467
  ...(config.dependencies && config.dependencies.length > 0 ? { dependencies: config.dependencies } : {}),
1290
1468
  };
1291
1469
  await writeJson(metaFile, meta);
@@ -1298,6 +1476,7 @@ async function ensureInstanceConfigured(root, name, config) {
1298
1476
  ...config.providerKeys,
1299
1477
  });
1300
1478
  await syncInstanceMeta(root, name, config.port, {
1479
+ databaseIntent: config.databaseIntent,
1301
1480
  ...(config.dependencies !== undefined ? { dependencies: config.dependencies } : {}),
1302
1481
  });
1303
1482
  return { envFile, instanceDir, created };
@@ -1420,6 +1599,10 @@ function resolveRecentMessages(args) {
1420
1599
  }
1421
1600
  return [];
1422
1601
  }
1602
+ function resolveBackfillFile(args) {
1603
+ const backfill = getFlag(args, 'backfill')?.trim();
1604
+ return backfill ? path_1.default.resolve(backfill) : null;
1605
+ }
1423
1606
  function parsePositiveInteger(raw, label) {
1424
1607
  if (!raw)
1425
1608
  return undefined;
@@ -1529,7 +1712,10 @@ function printHandshakeResult(target, task, result) {
1529
1712
  console.log(` memory facts ${result.workingMemory.length}`);
1530
1713
  console.log(` generated ${result.briefGeneratedAt}`);
1531
1714
  console.log('');
1532
- console.log(`Rules: ${truncateText(result.operatingRules, 160)}`);
1715
+ console.log('Rules:');
1716
+ for (const line of result.operatingRules.split(/\r?\n/)) {
1717
+ console.log(line.trim().length > 0 ? ` ${line}` : '');
1718
+ }
1533
1719
  if (result.workingMemory.length === 0) {
1534
1720
  console.log('');
1535
1721
  console.log('No working memory entries loaded.');
@@ -1549,6 +1735,9 @@ function printAttendResult(target, latestMessage, result) {
1549
1735
  console.log(` message ${truncateText(latestMessage, 120)}`);
1550
1736
  console.log(` inject ${result.shouldInject ? 'yes' : 'no'}`);
1551
1737
  console.log(` reason ${result.reason}`);
1738
+ if (result.bootstrap?.handshakePerformed) {
1739
+ console.log(` bootstrap handshake auto-ran (${result.bootstrap.task})`);
1740
+ }
1552
1741
  console.log(` method ${result.decision.method}`);
1553
1742
  console.log(` confidence ${result.decision.confidence}`);
1554
1743
  console.log(` explanation ${result.decision.explanation}`);
@@ -1879,12 +2068,24 @@ async function syncInstanceMeta(root, name, port, options = {}) {
1879
2068
  const existing = await readInstanceMetaFile(metaFile);
1880
2069
  const existingCreatedAt = typeof existing?.createdAt === 'string' ? existing.createdAt : undefined;
1881
2070
  const existingDependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(existing?.dependencies).dependencies;
2071
+ const currentDependencies = options.dependencies ?? existingDependencies;
2072
+ const env = fs_1.default.existsSync(envFile)
2073
+ ? await readEnvFile(envFile).catch(() => ({}))
2074
+ : {};
2075
+ const resolvedDatabaseIntent = options.databaseIntent
2076
+ ?? resolveInstanceDatabaseIntent({
2077
+ instanceName: name,
2078
+ env,
2079
+ meta: existing,
2080
+ dependencies: currentDependencies,
2081
+ }).intent;
1882
2082
  const meta = {
1883
2083
  name,
1884
2084
  createdAt: existingCreatedAt ?? new Date().toISOString(),
1885
2085
  port,
1886
2086
  envFile,
1887
2087
  instanceDir,
2088
+ ...(resolvedDatabaseIntent ? { databaseIntent: resolvedDatabaseIntent } : {}),
1888
2089
  ...(options.dependencies !== undefined
1889
2090
  ? { dependencies: options.dependencies }
1890
2091
  : existingDependencies.length > 0
@@ -1992,6 +2193,7 @@ async function executeSetupPlan(plan) {
1992
2193
  const configured = await ensureInstanceConfigured(plan.root, plan.instanceName, {
1993
2194
  port: plan.port,
1994
2195
  dbUrl: plan.databaseUrl,
2196
+ databaseIntent: plan.databaseIntent,
1995
2197
  provider: plan.provider,
1996
2198
  providerKeys: plan.providerKeys,
1997
2199
  apiKey: plan.apiKey,
@@ -2070,6 +2272,7 @@ async function executeSetupPlan(plan) {
2070
2272
  port: plan.port,
2071
2273
  mode: plan.mode,
2072
2274
  databaseMode: plan.databaseMode,
2275
+ databaseIntent: plan.databaseIntent,
2073
2276
  bindings,
2074
2277
  };
2075
2278
  }
@@ -2096,6 +2299,7 @@ function parseSetupConfig(filePath) {
2096
2299
  ? 'local'
2097
2300
  : (() => { throw new Error(`Unsupported databaseMode in setup config: ${databaseModeRaw}`); })();
2098
2301
  const databaseUrl = deriveDatabaseUrlForMode(databaseMode, instanceName, String(raw?.databaseUrl ?? raw?.dbUrl ?? '').trim());
2302
+ const databaseIntentStrategy = parseDatabaseIntentStrategyFlag(String(raw?.databaseIntent ?? raw?.dbIntent ?? '').trim(), 'databaseIntent');
2099
2303
  const provider = normalizeProvider(String(raw?.provider ?? 'mock')) ?? 'mock';
2100
2304
  if (!isSupportedProvider(provider)) {
2101
2305
  throw new Error(`Unsupported provider in setup config: ${provider}`);
@@ -2134,6 +2338,13 @@ function parseSetupConfig(filePath) {
2134
2338
  port,
2135
2339
  databaseUrl,
2136
2340
  databaseMode,
2341
+ databaseIntent: buildInstanceDatabaseIntent({
2342
+ instanceName,
2343
+ databaseMode,
2344
+ databaseUrl,
2345
+ strategy: databaseIntentStrategy,
2346
+ dockerContainerName: typeof raw?.dockerContainerName === 'string' ? raw.dockerContainerName : undefined,
2347
+ }),
2137
2348
  provider,
2138
2349
  providerKeys,
2139
2350
  apiKey,
@@ -2174,6 +2385,7 @@ function defaultsSetupPlan(args) {
2174
2385
  throw new Error(`Invalid --db-mode '${explicit}'. Use local, managed, or docker.`);
2175
2386
  })();
2176
2387
  const databaseUrl = deriveDatabaseUrlForMode(databaseMode, instanceName, (getFlag(args, 'db-url') ?? process.env.DATABASE_URL ?? '').trim());
2388
+ const databaseIntentStrategy = parseDatabaseIntentStrategyFlag(getFlag(args, 'db-intent'), '--db-intent');
2177
2389
  const provider = normalizeProvider(getFlag(args, 'provider') ?? process.env.LLM_PROVIDER ?? 'mock') ?? 'mock';
2178
2390
  if (!isSupportedProvider(provider)) {
2179
2391
  throw new Error(`Unsupported provider '${provider}' for --defaults.`);
@@ -2215,6 +2427,13 @@ function defaultsSetupPlan(args) {
2215
2427
  port,
2216
2428
  databaseUrl,
2217
2429
  databaseMode,
2430
+ databaseIntent: buildInstanceDatabaseIntent({
2431
+ instanceName,
2432
+ databaseMode,
2433
+ databaseUrl,
2434
+ strategy: databaseIntentStrategy,
2435
+ dockerContainerName: getFlag(args, 'docker-container-name'),
2436
+ }),
2218
2437
  provider,
2219
2438
  providerKeys,
2220
2439
  apiKey,
@@ -2658,12 +2877,28 @@ async function spawnDetachedCli(args, cwd) {
2658
2877
  child.unref();
2659
2878
  return pid;
2660
2879
  }
2661
- async function waitForRestartedRuntime(runtimeFile, previousPid, timeoutMs) {
2880
+ function isFreshRestartState(inspection, previousPid, previousStartedAt) {
2881
+ if (!inspection.running || !inspection.state)
2882
+ return false;
2883
+ if (inspection.state.pid && inspection.state.pid !== previousPid) {
2884
+ return true;
2885
+ }
2886
+ if (!previousStartedAt) {
2887
+ return false;
2888
+ }
2889
+ const previousStartedMs = Date.parse(previousStartedAt);
2890
+ const currentStartedMs = Date.parse(inspection.state.startedAt);
2891
+ if (!Number.isFinite(previousStartedMs) || !Number.isFinite(currentStartedMs)) {
2892
+ return false;
2893
+ }
2894
+ return currentStartedMs > previousStartedMs;
2895
+ }
2896
+ async function waitForRestartedRuntime(runtimeFile, previousPid, previousStartedAt, timeoutMs) {
2662
2897
  const startedAt = Date.now();
2663
2898
  let lastInspection = await (0, runtimeLifecycle_1.inspectRuntimeState)(runtimeFile);
2664
2899
  while (Date.now() - startedAt < timeoutMs) {
2665
2900
  lastInspection = await (0, runtimeLifecycle_1.inspectRuntimeState)(runtimeFile);
2666
- if (lastInspection.running && lastInspection.state?.pid && lastInspection.state.pid !== previousPid) {
2901
+ if (isFreshRestartState(lastInspection, previousPid, previousStartedAt)) {
2667
2902
  return lastInspection;
2668
2903
  }
2669
2904
  if (lastInspection.classification === 'invalid') {
@@ -2689,6 +2924,7 @@ async function restartInstanceRuntime(args, instanceName, scope, root) {
2689
2924
  }
2690
2925
  }
2691
2926
  const runtimeFile = instancePaths(root, instanceName).runtimeFile;
2927
+ const startupTimeoutMs = Math.max(timeoutMs, 30000);
2692
2928
  const newPid = await spawnDetachedCli([
2693
2929
  'run',
2694
2930
  '--instance',
@@ -2698,7 +2934,7 @@ async function restartInstanceRuntime(args, instanceName, scope, root) {
2698
2934
  '--root',
2699
2935
  root,
2700
2936
  ], root);
2701
- await waitForRestartedRuntime(runtimeFile, previousPid, 15000);
2937
+ await waitForRestartedRuntime(runtimeFile, previousPid, runtimeBefore.state?.startedAt ?? null, startupTimeoutMs);
2702
2938
  return {
2703
2939
  previousPid,
2704
2940
  newPid,
@@ -4120,6 +4356,7 @@ async function setupCommand(args) {
4120
4356
  console.log(` instance url http://localhost:${result.port}`);
4121
4357
  console.log(` memory mode ${result.mode}`);
4122
4358
  console.log(` database mode ${result.databaseMode}`);
4359
+ console.log(` db strategy ${describeInstanceDatabaseIntent(result.databaseIntent)}`);
4123
4360
  if (result.bindings.length === 0) {
4124
4361
  console.log(` projects ${paint('none bound yet', 'yellow')}`);
4125
4362
  }
@@ -4207,6 +4444,9 @@ async function setupCommand(args) {
4207
4444
  const existingInstance = fs_1.default.existsSync(instancePaths(finalRoot, instanceName).envFile)
4208
4445
  ? await loadInstanceEnv(finalRoot, instanceName)
4209
4446
  : null;
4447
+ const existingInstanceMeta = fs_1.default.existsSync(instancePaths(finalRoot, instanceName).metaFile)
4448
+ ? await readInstanceMetaFile(instancePaths(finalRoot, instanceName).metaFile)
4449
+ : null;
4210
4450
  if (existingInstance) {
4211
4451
  console.log(`${infoLabel()} Found existing instance '${instanceName}'. Updating it.`);
4212
4452
  }
@@ -4227,6 +4467,7 @@ async function setupCommand(args) {
4227
4467
  let databaseProvisioned = false;
4228
4468
  let dockerContainerName;
4229
4469
  let databaseMode = recommendedDatabaseMode;
4470
+ let databaseIntentStrategy = null;
4230
4471
  printChoiceGuide('Database Mode Choices', [
4231
4472
  {
4232
4473
  choice: 'local',
@@ -4266,6 +4507,13 @@ async function setupCommand(args) {
4266
4507
  console.log(`${infoLabel()} Iranti will create the local database automatically if it does not already exist.`);
4267
4508
  }
4268
4509
  }
4510
+ databaseIntentStrategy = parseDatabaseIntentStrategyFlag(await prompt.line('Database intent (dedicated, shared, external)', databaseIntentPromptValue(resolveInstanceDatabaseIntent({
4511
+ instanceName,
4512
+ env: existingInstance?.env ?? { DATABASE_URL: dbUrl },
4513
+ meta: existingInstanceMeta,
4514
+ dependencies: (0, runtimeDependencies_1.parseInstanceDependencies)(existingInstanceMeta?.dependencies).dependencies,
4515
+ }).intent?.strategy
4516
+ ?? inferDatabaseIntentStrategy({ instanceName, databaseMode, databaseUrl: dbUrl }))), 'database intent');
4269
4517
  bootstrapDatabase = await promptYesNo(prompt, 'Run migrations and seed the database now?', true);
4270
4518
  break;
4271
4519
  }
@@ -4281,6 +4529,15 @@ async function setupCommand(args) {
4281
4529
  const containerName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Docker container name', `iranti_${instanceName}_db`), `iranti_${instanceName}_db`);
4282
4530
  dockerContainerName = containerName;
4283
4531
  dbUrl = `postgresql://postgres:${dbPassword}@localhost:${dbHostPort}/${dbName}`;
4532
+ databaseIntentStrategy = parseDatabaseIntentStrategyFlag(await prompt.line('Database intent (dedicated, shared, external)', databaseIntentPromptValue(resolveInstanceDatabaseIntent({
4533
+ instanceName,
4534
+ env: existingInstance?.env ?? { DATABASE_URL: dbUrl },
4535
+ meta: existingInstanceMeta,
4536
+ dependencies: dockerContainerName
4537
+ ? [{ kind: 'docker-container', name: dockerContainerName, ...(dbHostPort > 0 ? { healthTcpPort: dbHostPort } : {}) }]
4538
+ : (0, runtimeDependencies_1.parseInstanceDependencies)(existingInstanceMeta?.dependencies).dependencies,
4539
+ }).intent?.strategy
4540
+ ?? inferDatabaseIntentStrategy({ instanceName, databaseMode, databaseUrl: dbUrl }))), 'database intent');
4284
4541
  console.log(`${infoLabel()} Docker will be used only for PostgreSQL. Iranti itself does not require Docker once a PostgreSQL database is available.`);
4285
4542
  if (await promptYesNo(prompt, `Start or reuse Docker container '${containerName}' now?`, true)) {
4286
4543
  await runDockerPostgresContainer({
@@ -4425,6 +4682,13 @@ async function setupCommand(args) {
4425
4682
  port,
4426
4683
  databaseUrl: dbUrl,
4427
4684
  databaseMode,
4685
+ databaseIntent: buildInstanceDatabaseIntent({
4686
+ instanceName,
4687
+ databaseMode,
4688
+ databaseUrl: dbUrl,
4689
+ strategy: databaseIntentStrategy,
4690
+ dockerContainerName,
4691
+ }),
4428
4692
  provider,
4429
4693
  providerKeys,
4430
4694
  apiKey: defaultApiKey,
@@ -4455,6 +4719,7 @@ async function setupCommand(args) {
4455
4719
  console.log(` instance url http://localhost:${finalResult.port}`);
4456
4720
  console.log(` memory mode ${finalResult.mode}`);
4457
4721
  console.log(` database mode ${finalResult.databaseMode}`);
4722
+ console.log(` db strategy ${describeInstanceDatabaseIntent(finalResult.databaseIntent)}`);
4458
4723
  if (finalResult.bindings.length === 0) {
4459
4724
  console.log(` projects ${paint('none bound yet', 'yellow')}`);
4460
4725
  }
@@ -5219,6 +5484,12 @@ async function showInstanceCommand(args) {
5219
5484
  : {};
5220
5485
  const meta = await readInstanceMetaFile(instancePaths(root, name).metaFile);
5221
5486
  const dependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(meta?.dependencies).dependencies;
5487
+ const databaseIntent = resolveInstanceDatabaseIntent({
5488
+ instanceName: name,
5489
+ env,
5490
+ meta,
5491
+ dependencies,
5492
+ });
5222
5493
  const runtime = await readInstanceRuntimeSummary(root, name);
5223
5494
  console.log(bold(`Instance: ${name}`));
5224
5495
  console.log(` dir : ${instanceDir}`);
@@ -5226,6 +5497,9 @@ async function showInstanceCommand(args) {
5226
5497
  console.log(` config: ${describeInstanceConfig(config)}`);
5227
5498
  console.log(` port: ${env.IRANTI_PORT ?? '3001'}`);
5228
5499
  console.log(` db : ${env.DATABASE_URL ?? '(missing)'}`);
5500
+ if (databaseIntent.intent) {
5501
+ console.log(` db strategy: ${describeInstanceDatabaseIntent(databaseIntent.intent, databaseIntent.source)}`);
5502
+ }
5229
5503
  console.log(` esc : ${env.IRANTI_ESCALATION_DIR ?? '(missing)'}`);
5230
5504
  if (dependencies.length > 0) {
5231
5505
  console.log(` deps: ${dependencies.map((dependency) => (0, runtimeDependencies_1.describeInstanceDependency)(dependency)).join(', ')}`);
@@ -5354,6 +5628,12 @@ async function configureInstanceCommand(args) {
5354
5628
  const { instanceDir, envFile, env, config } = await loadInstanceEnv(root, name, { allowRepair: true });
5355
5629
  const currentMeta = await readInstanceMetaFile(instancePaths(root, name).metaFile);
5356
5630
  const currentDependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(currentMeta?.dependencies).dependencies;
5631
+ const currentDatabaseIntent = resolveInstanceDatabaseIntent({
5632
+ instanceName: name,
5633
+ env,
5634
+ meta: currentMeta,
5635
+ dependencies: currentDependencies,
5636
+ });
5357
5637
  const updates = {};
5358
5638
  let portRaw = getFlag(args, 'port');
5359
5639
  let dbUrl = getFlag(args, 'db-url');
@@ -5363,6 +5643,7 @@ async function configureInstanceCommand(args) {
5363
5643
  let clearProviderKey = hasFlag(args, 'clear-provider-key');
5364
5644
  let dockerContainerName = getFlag(args, 'docker-container');
5365
5645
  let dockerHealthPortRaw = getFlag(args, 'docker-health-port');
5646
+ let dbIntentRaw = getFlag(args, 'db-intent');
5366
5647
  const clearDockerContainer = hasFlag(args, 'clear-docker-container');
5367
5648
  if (hasFlag(args, 'interactive')) {
5368
5649
  await withPromptSession(async (prompt) => {
@@ -5370,12 +5651,14 @@ async function configureInstanceCommand(args) {
5370
5651
  'This updates one existing instance in place.',
5371
5652
  'API port controls where the Iranti API listens.',
5372
5653
  'DATABASE_URL points at the PostgreSQL database for this instance.',
5654
+ 'Database intent tells Iranti whether this should be treated as a dedicated local DB, a shared local DB, or an external existing DB.',
5373
5655
  'LLM provider and provider key control which model backend Iranti uses.',
5374
5656
  'Iranti API key is the client credential other tools and project bindings use to authenticate.',
5375
5657
  'Optional Docker dependency settings let `iranti run --instance` start a recorded backing container before the API boots.',
5376
5658
  ]);
5377
5659
  portRaw = await prompt.line('API port', portRaw ?? env.IRANTI_PORT);
5378
5660
  dbUrl = await prompt.line('DATABASE_URL', dbUrl ?? env.DATABASE_URL);
5661
+ dbIntentRaw = await prompt.line('Database intent (dedicated, shared, external)', dbIntentRaw ?? databaseIntentPromptValue(currentDatabaseIntent.intent?.strategy));
5379
5662
  providerInput = await prompt.line('LLM provider', providerInput ?? env.LLM_PROVIDER ?? 'mock');
5380
5663
  const interactiveProvider = normalizeProvider(providerInput ?? env.LLM_PROVIDER ?? 'mock');
5381
5664
  const interactiveProviderEnvKey = providerKeyEnv(interactiveProvider);
@@ -5398,6 +5681,7 @@ async function configureInstanceCommand(args) {
5398
5681
  }
5399
5682
  if (dbUrl)
5400
5683
  updates.DATABASE_URL = dbUrl;
5684
+ const requestedDatabaseIntent = parseDatabaseIntentStrategyFlag(dbIntentRaw, '--db-intent');
5401
5685
  if (apiKey)
5402
5686
  updates.IRANTI_API_KEY = apiKey;
5403
5687
  const provider = normalizeProvider(providerInput ?? env.LLM_PROVIDER ?? 'mock');
@@ -5450,8 +5734,9 @@ async function configureInstanceCommand(args) {
5450
5734
  }
5451
5735
  if (Object.keys(updates).length === 0) {
5452
5736
  const dependenciesUnchanged = JSON.stringify(nextDependencies) === JSON.stringify(currentDependencies);
5453
- if (dependenciesUnchanged) {
5454
- throw new Error('No changes provided. Use flags like --provider, --provider-key, --api-key, --db-url, --port, or the Docker dependency flags.');
5737
+ const databaseIntentUnchanged = requestedDatabaseIntent === null;
5738
+ if (dependenciesUnchanged && databaseIntentUnchanged) {
5739
+ throw new Error('No changes provided. Use flags like --provider, --provider-key, --api-key, --db-url, --db-intent, --port, or the Docker dependency flags.');
5455
5740
  }
5456
5741
  }
5457
5742
  const nextEnv = { ...env };
@@ -5471,9 +5756,19 @@ async function configureInstanceCommand(args) {
5471
5756
  if (!nextEnv.DATABASE_URL?.trim()) {
5472
5757
  throw cliError('IRANTI_INSTANCE_DATABASE_REQUIRED', `Instance '${name}' still needs DATABASE_URL before it can be considered repaired.`, ['Pass `--db-url <postgresql://...>` or rerun `iranti configure instance <name> --interactive`.'], { instance: name, envFile, config: config.classification });
5473
5758
  }
5759
+ const nextDatabaseIntent = buildInstanceDatabaseIntent({
5760
+ instanceName: name,
5761
+ databaseMode: inferDatabaseProvisioning(nextEnv.DATABASE_URL, nextDependencies),
5762
+ databaseUrl: nextEnv.DATABASE_URL,
5763
+ strategy: requestedDatabaseIntent ?? currentDatabaseIntent.intent?.strategy,
5764
+ dockerContainerName: nextDependencies.find((dependency) => dependency.kind === 'docker-container')?.name,
5765
+ });
5474
5766
  await ensureDir(instanceDir);
5475
5767
  await upsertEnvFile(envFile, updates);
5476
- await syncInstanceMeta(root, name, nextPort, { dependencies: nextDependencies });
5768
+ await syncInstanceMeta(root, name, nextPort, {
5769
+ dependencies: nextDependencies,
5770
+ databaseIntent: nextDatabaseIntent,
5771
+ });
5477
5772
  const json = hasFlag(args, 'json');
5478
5773
  const result = {
5479
5774
  instance: name,
@@ -5482,6 +5777,7 @@ async function configureInstanceCommand(args) {
5482
5777
  provider: updates.LLM_PROVIDER ?? env.LLM_PROVIDER ?? 'mock',
5483
5778
  apiKeyChanged: Boolean(apiKey),
5484
5779
  providerKeyChanged: Boolean(providerKey) || hasFlag(args, 'clear-provider-key'),
5780
+ databaseIntent: nextDatabaseIntent.strategy,
5485
5781
  dependencies: nextDependencies.map((dependency) => (0, runtimeDependencies_1.describeInstanceDependency)(dependency)),
5486
5782
  };
5487
5783
  if (json) {
@@ -5492,6 +5788,7 @@ async function configureInstanceCommand(args) {
5492
5788
  console.log(` status ${okLabel()}`);
5493
5789
  console.log(` env ${envFile}`);
5494
5790
  console.log(` keys ${result.updatedKeys.join(', ')}`);
5791
+ console.log(` db ${describeInstanceDatabaseIntent(nextDatabaseIntent)}`);
5495
5792
  if (result.dependencies.length > 0) {
5496
5793
  console.log(` deps ${result.dependencies.join(', ')}`);
5497
5794
  }
@@ -5731,7 +6028,24 @@ async function handshakeCommand(args) {
5731
6028
  const json = hasFlag(args, 'json');
5732
6029
  const target = await resolveAttendantCliTarget(args);
5733
6030
  const task = getFlag(args, 'task')?.trim() || 'CLI handshake';
5734
- const recentMessages = resolveRecentMessages(args);
6031
+ let recentMessages = resolveRecentMessages(args);
6032
+ const backfillFile = resolveBackfillFile(args);
6033
+ let backfillResult = null;
6034
+ if (backfillFile) {
6035
+ const content = fs_1.default.readFileSync(backfillFile, 'utf-8');
6036
+ backfillResult = await (0, autoRemember_1.backfillChatHistory)({
6037
+ iranti: target.iranti,
6038
+ content,
6039
+ agent: target.agentId,
6040
+ source: 'CLIBackfill',
6041
+ });
6042
+ if (recentMessages.length === 0) {
6043
+ recentMessages = (0, autoRemember_1.parseBackfillChatTranscript)(content)
6044
+ .map((message) => message.text.trim())
6045
+ .filter(Boolean)
6046
+ .slice(-12);
6047
+ }
6048
+ }
5735
6049
  const result = await target.iranti.handshake({
5736
6050
  agent: target.agentId,
5737
6051
  task,
@@ -5743,12 +6057,33 @@ async function handshakeCommand(args) {
5743
6057
  envSource: target.envSource,
5744
6058
  envFile: target.envFile,
5745
6059
  task,
6060
+ backfillFile,
6061
+ backfillResult,
5746
6062
  recentMessages,
5747
6063
  result,
5748
6064
  }, null, 2));
5749
6065
  return;
5750
6066
  }
5751
6067
  printHandshakeResult(target, task, result);
6068
+ if (backfillResult) {
6069
+ console.log('');
6070
+ console.log('Backfill:');
6071
+ console.log(` file ${backfillFile}`);
6072
+ console.log(` messages ${backfillResult.messagesParsed}`);
6073
+ console.log(` extracted ${backfillResult.extracted}`);
6074
+ console.log(` written ${backfillResult.written}`);
6075
+ if (backfillResult.skipped.length > 0) {
6076
+ console.log(` skipped ${backfillResult.skipped.length}`);
6077
+ }
6078
+ }
6079
+ if (result.backfillSuggestion?.suggested && !backfillFile) {
6080
+ console.log('');
6081
+ console.log('Backfill suggestion:');
6082
+ console.log(` reason ${result.backfillSuggestion.reason}`);
6083
+ console.log(` candidateFacts ${result.backfillSuggestion.candidateFacts}`);
6084
+ console.log(` sample keys ${result.backfillSuggestion.sampleKeys.join(', ')}`);
6085
+ console.log(` command ${result.backfillSuggestion.suggestedCommand}`);
6086
+ }
5752
6087
  console.log('');
5753
6088
  console.log(`${infoLabel()} This is a manual Attendant inspection tool. Claude Code should still use hooks + MCP in normal operation.`);
5754
6089
  }