@north-light/crouter 0.3.316 → 0.3.317

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.
@@ -298,14 +298,15 @@ interface FakeServices {
298
298
  resourceLoader: ResourceLoader;
299
299
  modelRuntime: FakeModelRuntime;
300
300
  }
301
+ type FakeResourceLoaderOptions = {
302
+ additionalExtensionPaths?: string[];
303
+ appendSystemPrompt?: string[];
304
+ };
301
305
  export declare function createAgentSessionServices(options: {
302
306
  cwd: string;
303
307
  agentDir?: string;
304
308
  modelRuntime?: unknown;
305
- resourceLoaderOptions?: {
306
- additionalExtensionPaths?: string[];
307
- appendSystemPrompt?: string[];
308
- };
309
+ resourceLoaderOptions?: FakeResourceLoaderOptions;
309
310
  }): Promise<FakeServices>;
310
311
  export declare function createAgentSessionFromServices(options: {
311
312
  services: FakeServices;
@@ -31,11 +31,12 @@
31
31
  // <nodeDir>/fake-pi.dialog.jsonl — one line per resolved unattended dialog (§5.4)
32
32
  import { spawn } from 'node:child_process';
33
33
  import { writeFileSync, readFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync, } from 'node:fs';
34
- import { join, dirname } from 'node:path';
34
+ import { join, dirname, resolve } from 'node:path';
35
35
  import { randomUUID } from 'node:crypto';
36
- import { DefaultResourceLoader, getAgentDir } from '@earendil-works/pi-coding-agent';
36
+ import { DefaultPackageManager, DefaultResourceLoader, getAgentDir, SettingsManager } from '@earendil-works/pi-coding-agent';
37
37
  import { MANAGED_PROVIDER_COOLING_DIAGNOSTIC } from '../../runtime/managed-provider-cooling.js';
38
38
  import { PROVIDER_FAILURE_DIAGNOSTIC } from '../../provider-failure.js';
39
+ import { CANVAS_EXTENSIONS } from '../../runtime/canvas-extensions.js';
39
40
  import registerCanvasBashValve, { createValveOperations } from '../../../pi-extensions/canvas-bash-valve.js';
40
41
  /** Test seams for prompt-scoped failures before session_start. The one-shot
41
42
  * variant records its first failure per node so the next cycling retry can
@@ -678,6 +679,14 @@ class FakeSession {
678
679
  // A turn consumes whatever was queued for it, as the real session does.
679
680
  this.steering.length = 0;
680
681
  this.followUps.length = 0;
682
+ let releaseGate;
683
+ if (process.env['PI_FAKE_ENGINE_HOLD_TURN'] === '1') {
684
+ let release;
685
+ let resolveSettled;
686
+ const settled = new Promise((resolve) => { resolveSettled = resolve; });
687
+ releaseGate = new Promise((resolve) => { release = resolve; });
688
+ this.heldTurn = { release, settled, resolveSettled };
689
+ }
681
690
  this.streaming = true;
682
691
  if (opts.runHooks === true) {
683
692
  await this.fire('agent_start', {}, this.buildCtx());
@@ -685,14 +694,7 @@ class FakeSession {
685
694
  this.emit({ type: 'agent_start' });
686
695
  this.emit({ type: 'turn_start' });
687
696
  this.emit({ type: 'message_start', message: small });
688
- if (process.env['PI_FAKE_ENGINE_HOLD_TURN'] === '1') {
689
- let release;
690
- let resolveSettled;
691
- const settled = new Promise((resolve) => { resolveSettled = resolve; });
692
- const releaseGate = new Promise((resolve) => { release = resolve; });
693
- this.heldTurn = { release, settled, resolveSettled };
694
- await releaseGate;
695
- }
697
+ await releaseGate;
696
698
  // F2 (message_update coalescing): the broker now coalesces message_update
697
699
  // relays (latest-wins, ~75 ms window), so a padded flood riding message_update
698
700
  // can no longer climb a stalled viewer's backlog — the coalescer would shed
@@ -1291,14 +1293,93 @@ const FAKE_REGISTERED_MODELS = [
1291
1293
  { provider: 'openai-codex', id: 'gpt-5.6-luna' },
1292
1294
  FAKE_AVAILABLE_MODEL,
1293
1295
  ];
1296
+ /** Source harnesses use native imports only for an isolated, mandatory-only
1297
+ * canvas fixture. Every other source stays on the SDK file-loader path. */
1298
+ async function sourceCanvasExtensionLoaderOptions(cwd, agentDir, options) {
1299
+ const extensionPaths = options?.additionalExtensionPaths;
1300
+ const mandatoryOnly = extensionPaths?.length === CANVAS_EXTENSIONS.length
1301
+ && extensionPaths.every((path, index) => path === CANVAS_EXTENSIONS[index]);
1302
+ if (!import.meta.url.endsWith('.ts') || !mandatoryOnly)
1303
+ return { additionalExtensionPaths: extensionPaths };
1304
+ const settingsManager = SettingsManager.create(cwd, agentDir);
1305
+ const resolved = await new DefaultPackageManager({ cwd, agentDir, settingsManager }).resolve();
1306
+ if (resolved.extensions.some((extension) => extension.enabled)) {
1307
+ return { additionalExtensionPaths: extensionPaths, settingsManager };
1308
+ }
1309
+ const inlinePaths = new Map();
1310
+ const fileLoaderErrors = new Map();
1311
+ const missingPaths = [];
1312
+ const extensionFactories = [];
1313
+ for (const inputPath of extensionPaths) {
1314
+ const path = resolve(cwd, inputPath);
1315
+ if (!existsSync(path)) {
1316
+ missingPaths.push(path);
1317
+ continue;
1318
+ }
1319
+ inlinePaths.set(`<inline:${path}>`, path);
1320
+ let factory;
1321
+ try {
1322
+ const loaded = await import(path);
1323
+ if (typeof loaded.default !== 'function') {
1324
+ fileLoaderErrors.set(path, `Extension does not export a valid factory function: ${path}`);
1325
+ factory = () => { throw new Error(fileLoaderErrors.get(path)); };
1326
+ }
1327
+ else {
1328
+ factory = loaded.default;
1329
+ }
1330
+ }
1331
+ catch {
1332
+ // Let the original loader produce its own import/transform diagnostic.
1333
+ return { additionalExtensionPaths: extensionPaths, settingsManager };
1334
+ }
1335
+ extensionFactories.push({ name: path, factory });
1336
+ }
1337
+ return {
1338
+ additionalExtensionPaths: [],
1339
+ extensionFactories,
1340
+ settingsManager,
1341
+ restoreCliExtensionMetadata: (loader) => {
1342
+ const result = loader.getExtensions();
1343
+ for (const extension of result.extensions) {
1344
+ const path = inlinePaths.get(extension.path);
1345
+ if (path === undefined)
1346
+ continue;
1347
+ const sourceInfo = { path, source: 'cli', scope: 'temporary', origin: 'top-level', baseDir: undefined };
1348
+ extension.path = path;
1349
+ extension.resolvedPath = path;
1350
+ extension.sourceInfo = sourceInfo;
1351
+ for (const command of extension.commands.values())
1352
+ command.sourceInfo = sourceInfo;
1353
+ for (const tool of extension.tools.values())
1354
+ tool.sourceInfo = sourceInfo;
1355
+ }
1356
+ for (const error of result.errors) {
1357
+ const path = inlinePaths.get(error.path);
1358
+ if (path !== undefined) {
1359
+ error.path = path;
1360
+ const isConflict = /^(?:Tool|Flag) ".*" conflicts with /.test(error.error);
1361
+ error.error = fileLoaderErrors.get(path) ?? (isConflict ? error.error : `Failed to load extension: ${error.error}`);
1362
+ }
1363
+ for (const [inlinePath, filePath] of inlinePaths) {
1364
+ error.error = error.error.replaceAll(inlinePath, filePath);
1365
+ }
1366
+ }
1367
+ result.errors.push(...missingPaths.map((path) => ({ path, error: `Extension path does not exist: ${path}` })));
1368
+ },
1369
+ };
1370
+ }
1294
1371
  export async function createAgentSessionServices(options) {
1372
+ const agentDir = options.agentDir ?? getAgentDir();
1373
+ const extensionOptions = await sourceCanvasExtensionLoaderOptions(options.cwd, agentDir, options.resourceLoaderOptions);
1374
+ const { restoreCliExtensionMetadata, ...loaderOptions } = extensionOptions;
1295
1375
  const loader = new DefaultResourceLoader({
1296
1376
  cwd: options.cwd,
1297
- agentDir: options.agentDir ?? getAgentDir(),
1298
- additionalExtensionPaths: options.resourceLoaderOptions?.additionalExtensionPaths,
1377
+ agentDir,
1378
+ ...loaderOptions,
1299
1379
  appendSystemPrompt: options.resourceLoaderOptions?.appendSystemPrompt,
1300
1380
  });
1301
1381
  await loader.reload();
1382
+ restoreCliExtensionMetadata?.(loader);
1302
1383
  // Zero-auth test seam. While <CRTR_HOME>/fake-pi.no-auth exists, the registry
1303
1384
  // reports a GENUINE zero-auth boot (nothing registered, nothing authenticated)
1304
1385
  // so resolveLaunchModel (broker.ts) falls all the way to a tier-3 model-less
@@ -367,6 +367,8 @@ test('reload_auth re-drives a parked fatal provider-auth turn exactly once', { t
367
367
  await seedPool('openai-codex', false);
368
368
  const child = await h.spawnHeadlessChild(parent, 'auth reload child', { model: OPENAI_STRONG.spec });
369
369
  await h.waitFor(() => agentStartCount(child) >= 1, { label: 'initial auth fixture turn started' });
370
+ const kickoffObserver = await attachUntil(child, 'observer', 'auth-reload-kickoff-settled', (a) => a.welcome.snapshot.state.isStreaming === false, 'auth reload kickoff settled');
371
+ kickoffObserver.close();
370
372
  const baselineStarts = agentStartCount(child);
371
373
  h.fakeCmd(child, {
372
374
  cmd: 'stop',
@@ -397,6 +399,8 @@ test('reload_auth leaves a parked auth fault with its original session when navi
397
399
  await seedPool('openai-codex', false);
398
400
  const child = await h.spawnHeadlessChild(parent, 'auth replacement child', { model: OPENAI_STRONG.spec });
399
401
  await h.waitFor(() => agentStartCount(child) >= 1, { label: 'initial replacement fixture turn started' });
402
+ const kickoffObserver = await attachUntil(child, 'observer', 'auth-replacement-kickoff-settled', (a) => a.welcome.snapshot.state.isStreaming === false, 'auth replacement kickoff settled');
403
+ kickoffObserver.close();
400
404
  h.fakeCmd(child, {
401
405
  cmd: 'stop',
402
406
  reason: 'error',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.316",
3
+ "version": "0.3.317",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.316",
3
+ "version": "0.3.317",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.316",
9
+ "version": "0.3.317",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "workspaces": [
@@ -5206,17 +5206,17 @@
5206
5206
  },
5207
5207
  "packages/crouter-api": {
5208
5208
  "name": "@north-light/crouter-api",
5209
- "version": "0.3.316",
5209
+ "version": "0.3.317",
5210
5210
  "license": "UNLICENSED"
5211
5211
  },
5212
5212
  "packages/crouter-env-docker": {
5213
5213
  "name": "@north-light/crouter-env-docker",
5214
- "version": "0.3.316",
5214
+ "version": "0.3.317",
5215
5215
  "license": "UNLICENSED"
5216
5216
  },
5217
5217
  "packages/crouter-sdk": {
5218
5218
  "name": "@north-light/crouter-sdk",
5219
- "version": "0.3.316",
5219
+ "version": "0.3.317",
5220
5220
  "license": "UNLICENSED",
5221
5221
  "dependencies": {
5222
5222
  "@north-light/crouter-api": "^0.3.295"