larkin 0.4.1 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +76 -33
  2. package/artifacts/release/THIRD_PARTY_NOTICES.txt +218 -228
  3. package/assets/readme/larkin-github-cover.png +0 -0
  4. package/assets/readme/larkin-hero.svg +111 -0
  5. package/dist/agent/agent-state-store.cjs +193 -1
  6. package/dist/agent/agent-state-store.mjs +191 -0
  7. package/dist/app/agent-config.mjs +73 -6
  8. package/dist/app/agent-readiness.cjs +22 -2
  9. package/dist/app/agent-readiness.mjs +20 -2
  10. package/dist/app/binary-entry.mjs +6 -0
  11. package/dist/app/cli.mjs +7 -1
  12. package/dist/app/local-control.mjs +108 -2
  13. package/dist/app/run.mjs +8 -0
  14. package/dist/app/runtime-process.mjs +17 -1
  15. package/dist/app/session-cli.mjs +55 -20
  16. package/dist/dashboard/dashboard-view-model.mjs +12 -3
  17. package/dist/feishu/host-shell.cjs +104 -9
  18. package/dist/feishu/host-shell.mjs +105 -10
  19. package/dist/platform/config.cjs +406 -33
  20. package/dist/platform/config.mjs +405 -33
  21. package/dist/platform/process-boundary-trace.cjs +108 -0
  22. package/dist/platform/process-boundary-trace.mjs +102 -0
  23. package/dist/runtime/pi-artifact-provenance.cjs +147 -0
  24. package/dist/runtime/pi-artifact-provenance.mjs +139 -0
  25. package/dist/runtime/pi-compaction-recovery.cjs +703 -0
  26. package/dist/runtime/pi-compaction-recovery.mjs +679 -0
  27. package/dist/runtime/pi-model-catalog.cjs +2 -2
  28. package/dist/runtime/pi-model-catalog.mjs +2 -2
  29. package/dist/runtime/pi-profile-migration.cjs +556 -0
  30. package/dist/runtime/pi-profile-migration.mjs +515 -0
  31. package/dist/runtime/pi-provider-config.cjs +3 -3
  32. package/dist/runtime/pi-provider-config.mjs +3 -3
  33. package/dist/runtime/pi-rpc-client.cjs +39 -22
  34. package/dist/runtime/pi-rpc-client.mjs +39 -22
  35. package/dist/runtime/provider-error-classifier.cjs +39 -0
  36. package/dist/runtime/provider-error-classifier.mjs +34 -0
  37. package/dist/runtime/runtime-adapters.mjs +176 -27
  38. package/dist/runtime/runtime-host.cjs +630 -19
  39. package/dist/runtime/runtime-host.mjs +628 -18
  40. package/dist/runtime/runtime-readiness.cjs +6 -4
  41. package/dist/runtime/runtime-readiness.mjs +6 -4
  42. package/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import { spawn as nodeSpawn } from "node:child_process";
1
+ import { spawn as nodeSpawn, spawnSync } from "node:child_process";
2
2
  import crypto from "node:crypto";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
@@ -6,10 +6,14 @@ import { createInterface } from "node:readline";
6
6
  import { isPiThinkingLevel } from "./pi-model-catalog.mjs";
7
7
  import { PiRpcClient } from "./pi-rpc-client.mjs";
8
8
  import { internalCommandSpec } from "../app/internal-command.mjs";
9
- import { piAgentDirectory } from "./pi-provider-config.mjs";
9
+ import { BUNDLED_PI_VERSION, piAgentDirectory } from "./pi-provider-config.mjs";
10
+ import { recordPiRuntimeArtifactProvenance } from "./pi-artifact-provenance.mjs";
11
+ import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
10
12
  import { resolvePiSubagentExtensionArg } from "./pi-subagent-injection.mjs";
11
13
  import { resolvePiBashTimeoutExtensionArg } from "./pi-bash-timeout-injection.mjs";
12
14
  import { classifyRuntimePrerequisite, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
15
+ import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, } from "./provider-error-classifier.mjs";
16
+ import { assertEffectivePiCompactionSettings, assertNoProjectPiCompactionOverride, calculatePiCompactionSettings, parsePiExecutableVersion, prepareOwnedPiDirectory, readOwnedPiSettings, verifyPiCapabilities, writeOwnedPiSettings, } from "./pi-compaction-recovery.mjs";
13
17
  class EventSession {
14
18
  listeners = new Set();
15
19
  subscribe(listener) {
@@ -447,12 +451,15 @@ class PiSession extends EventSession {
447
451
  observedSubmitEpochs = new Set();
448
452
  observedAcceptedEpochs = new Set();
449
453
  observedCompletedEpochs = new Set();
454
+ observedAgentEndEpochs = new Set();
450
455
  firstOutputObserved = false;
451
456
  toolCallOpen = false;
457
+ piPolicyManaged;
452
458
  constructor(sdk, distribution) {
453
459
  super();
454
460
  this.sdk = sdk;
455
461
  this.distribution = distribution;
462
+ this.piPolicyManaged = sdk.policyManaged ?? typeof sdk.validatePolicy === "function";
456
463
  const result = sdk.subscribe?.((event) => this.onEvent(event));
457
464
  if (typeof result === "function")
458
465
  this.unsubscribe = result;
@@ -470,6 +477,25 @@ class PiSession extends EventSession {
470
477
  async prompt(input) { return this.enqueue(input, () => this.sdk.prompt(input.text)); }
471
478
  async busyInput(input) { return this.enqueue(input, () => this.sdk.steer(input.text)); }
472
479
  async cancel(_reason) { await this.sdk.abort(); }
480
+ async compact(customInstructions) {
481
+ if (!this.sdk.compact)
482
+ throw new Error("Pi compact RPC is unavailable");
483
+ await this.sdk.validatePolicy?.();
484
+ return this.sdk.compact(customInstructions);
485
+ }
486
+ async getContextUsage() {
487
+ if (!this.sdk.getSessionStats)
488
+ return null;
489
+ await this.sdk.validatePolicy?.();
490
+ const stats = await this.sdk.getSessionStats();
491
+ const usage = stats?.contextUsage;
492
+ const tokens = Number(usage?.tokens);
493
+ const contextWindow = Number(usage?.contextWindow);
494
+ if (!Number.isFinite(tokens) || tokens < 0 || !Number.isSafeInteger(contextWindow) || contextWindow <= 0) {
495
+ throw new Error("Pi get_session_stats returned invalid contextUsage");
496
+ }
497
+ return { tokens, contextWindow };
498
+ }
473
499
  async close(_reason) { this.unsubscribe?.(); await this.sdk.dispose?.(); }
474
500
  async enqueue(input, operation) {
475
501
  if (this.ownedInputIds.size === 0)
@@ -481,14 +507,15 @@ class PiSession extends EventSession {
481
507
  const ownsRpcObservation = !this.observedSubmitEpochs.has(epoch);
482
508
  if (ownsRpcObservation) {
483
509
  this.observedSubmitEpochs.add(epoch);
484
- this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "rpc_submit" });
510
+ this.emitObservation("rpc_submit");
485
511
  }
486
512
  try {
487
- await operation();
513
+ const accepted = this.sdk.validatePolicy ? this.sdk.validatePolicy().then(operation) : operation();
514
+ await accepted;
488
515
  this.awaitingAcknowledgement.delete(input.inputId);
489
516
  if (ownsRpcObservation && !this.observedAcceptedEpochs.has(epoch)) {
490
517
  this.observedAcceptedEpochs.add(epoch);
491
- this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "rpc_accepted" });
518
+ this.emitObservation("rpc_accepted");
492
519
  }
493
520
  return { status: "accepted", inputId: input.inputId };
494
521
  }
@@ -524,19 +551,23 @@ class PiSession extends EventSession {
524
551
  this.observedSubmitEpochs.clear();
525
552
  this.observedAcceptedEpochs.clear();
526
553
  this.observedCompletedEpochs.clear();
554
+ this.observedAgentEndEpochs.clear();
527
555
  this.activeEpoch = null;
528
556
  this.settleArmedEpoch = null;
529
557
  this.emit({ type: "error", message });
530
558
  }
531
- else if (event?.type === "compaction_start" && this.awaitingAcknowledgement.size > 0) {
532
- this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "compaction_start" });
559
+ else if (event?.type === "compaction_start") {
560
+ this.emitObservation("compaction_start", event.reason === "manual" || event.reason === "threshold" || event.reason === "overflow" ? { reason: event.reason } : {});
533
561
  }
534
- else if (event?.type === "compaction_end" && this.awaitingAcknowledgement.size > 0) {
535
- this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "compaction_end" });
562
+ else if (event?.type === "compaction_end") {
563
+ this.emitObservation("compaction_end", {
564
+ ...(event.reason === "manual" || event.reason === "threshold" || event.reason === "overflow" ? { reason: event.reason } : {}),
565
+ willRetry: event.willRetry === true, success: event.aborted !== true && event.result !== undefined,
566
+ });
536
567
  }
537
568
  else if ((event?.type === "auto_retry_start" || event?.type === "auto_retry_end"
538
569
  || String(event?.type || "").startsWith("summarization_retry_")) && this.awaitingAcknowledgement.size > 0) {
539
- this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "retry_progress" });
570
+ this.emitObservation("retry_progress");
540
571
  }
541
572
  else if (event?.type === "turn_start") {
542
573
  const epoch = this.oldestOwnedEpoch();
@@ -546,7 +577,7 @@ class PiSession extends EventSession {
546
577
  this.settleArmedEpoch = null;
547
578
  this.firstOutputObserved = false;
548
579
  this.toolCallOpen = false;
549
- this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "turn_start" });
580
+ this.emitObservation("turn_start");
550
581
  this.emit({ type: "turn-start", ...(Number.isInteger(event.turnIndex) ? { turnId: `pi-${event.turnIndex}` } : {}) });
551
582
  }
552
583
  else if (event?.type === "agent_end") {
@@ -557,9 +588,17 @@ class PiSession extends EventSession {
557
588
  ? piAssistantProviderError(assistant)
558
589
  : null;
559
590
  this.settleArmedEpoch = this.activeEpoch;
591
+ if (this.activeEpoch !== null && !this.observedAgentEndEpochs.has(this.activeEpoch)) {
592
+ this.observedAgentEndEpochs.add(this.activeEpoch);
593
+ this.emitObservation("agent_end", { willRetry: event.willRetry === true });
594
+ }
595
+ if (event.willRetry === true && this.finalAssistantError?.upstream.message === "Your input exceeds the context window of this model. Please adjust your input and try again.") {
596
+ this.emit({ type: "input-error", inputId: this.oldestOwnedInput(), retryable: true, willRetry: true,
597
+ message: "Pi owns the context-overflow retry", errorCategory: "context_window" });
598
+ }
560
599
  if (event.willRetry !== true && this.activeEpoch !== null && !this.observedCompletedEpochs.has(this.activeEpoch)) {
561
600
  this.observedCompletedEpochs.add(this.activeEpoch);
562
- this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "completed" });
601
+ this.emitObservation("completed");
563
602
  }
564
603
  }
565
604
  else if (event?.type === "agent_settled") {
@@ -576,7 +615,7 @@ class PiSession extends EventSession {
576
615
  this.toolCallOpen = false;
577
616
  this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "tool_result" });
578
617
  }
579
- this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "settled" });
618
+ this.emitObservation("settled");
580
619
  const owned = [...this.ownedInputIds].filter((inputId) => this.inputEpochs.get(inputId) === epoch);
581
620
  if (error) {
582
621
  const classified = classifyPiProviderError(error);
@@ -605,6 +644,7 @@ class PiSession extends EventSession {
605
644
  this.observedSubmitEpochs.delete(epoch);
606
645
  this.observedAcceptedEpochs.delete(epoch);
607
646
  this.observedCompletedEpochs.delete(epoch);
647
+ this.observedAgentEndEpochs.delete(epoch);
608
648
  }
609
649
  else if (event?.type === "tool_execution_start") {
610
650
  if (!this.firstOutputObserved) {
@@ -632,6 +672,20 @@ class PiSession extends EventSession {
632
672
  this.emit({ type: "activity", activity: kind, text: String(event.assistantMessageEvent.delta) });
633
673
  }
634
674
  }
675
+ emitObservation(phase, fields = {}) {
676
+ const inputId = this.oldestOwnedInput();
677
+ const observation = { type: "runtime-observation", runtime: "pi",
678
+ distribution: this.distribution, phase, ...fields };
679
+ // Correlation is host-internal metadata, not telemetry payload.
680
+ if (this.sessionId)
681
+ Object.defineProperty(observation, "sessionId", { value: this.sessionId, enumerable: false });
682
+ if (inputId)
683
+ Object.defineProperty(observation, "inputId", { value: inputId, enumerable: false });
684
+ this.emit(observation);
685
+ }
686
+ oldestOwnedInput() {
687
+ return this.ownedInputIds.values().next().value;
688
+ }
635
689
  oldestOwnedEpoch() {
636
690
  for (const inputId of this.ownedInputIds) {
637
691
  const epoch = this.inputEpochs.get(inputId);
@@ -675,6 +729,13 @@ export function classifyPiProviderError(error) {
675
729
  const errorType = "type" in error ? error.type : undefined;
676
730
  const reason = safeProviderText(upstream.message, "Pi provider request failed");
677
731
  const signal = new Set([String(upstream.code ?? "").trim().toLowerCase(), String(errorType ?? "").trim().toLowerCase()].filter(Boolean));
732
+ if (classifyStrictProviderError(buildStrictProviderErrorInput({
733
+ message: reason, type: errorType, upstream,
734
+ })) === "context_window")
735
+ return {
736
+ category: "context_window", reason: INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON,
737
+ nextAction: "Start a fresh Runtime session, then replay the retained Inbox deliveries.",
738
+ };
678
739
  if (upstream.status === 402 || [...signal].some((value) => ["payment_required", "billing_hard_limit_reached", "insufficient_balance"].includes(value)))
679
740
  return {
680
741
  category: "billing", reason, nextAction: "Update the provider billing method or balance, then retry.",
@@ -746,6 +807,28 @@ export function createPiSessionManager(input) {
746
807
  throw error;
747
808
  }
748
809
  }
810
+ async function discoverEffectivePiContextWindow(input, command, commandPrefix, requestedModel, env, spawn, rpcOptions) {
811
+ const probeArgs = [...commandPrefix, "--mode", "rpc", "--no-session", "--no-extensions",
812
+ ...(requestedModel ? ["--model", requestedModel] : [])];
813
+ const probe = spawn(command, probeArgs, {
814
+ cwd: input.workspaceDir,
815
+ env,
816
+ stdio: ["pipe", "pipe", "pipe"],
817
+ });
818
+ const client = new PiRpcClient(probe, rpcOptions);
819
+ try {
820
+ const state = await client.request("get_state");
821
+ const contextWindow = state.model?.contextWindow;
822
+ calculatePiCompactionSettings(contextWindow);
823
+ const model = state.model?.provider && state.model.id ? `${state.model.provider}/${state.model.id}` : null;
824
+ if (!model)
825
+ throw new Error("Pi isolated probe did not report an effective model");
826
+ return { model, contextWindow: contextWindow };
827
+ }
828
+ finally {
829
+ await client.close();
830
+ }
831
+ }
749
832
  function writePrivateAtomic(file, content) {
750
833
  try {
751
834
  if (fs.lstatSync(file).isSymbolicLink())
@@ -770,11 +853,15 @@ function writePrivateAtomic(file, content) {
770
853
  }
771
854
  class PiRpcBackend {
772
855
  client;
856
+ policy;
857
+ policyManaged;
773
858
  sessionId;
774
859
  model;
775
860
  thinkingLevel;
776
- constructor(client, state) {
861
+ constructor(client, state, policy) {
777
862
  this.client = client;
863
+ this.policy = policy;
864
+ this.policyManaged = Boolean(policy);
778
865
  this.sessionId = state.sessionId ?? null;
779
866
  if (state.model?.provider && state.model.id)
780
867
  this.model = {
@@ -787,6 +874,24 @@ class PiRpcBackend {
787
874
  prompt(text) { return this.client.request("prompt", { message: text }); }
788
875
  steer(text) { return this.client.request("steer", { message: text }); }
789
876
  abort() { return this.client.request("abort"); }
877
+ compact(customInstructions) { return this.client.requestCompact(customInstructions); }
878
+ getSessionStats() { return this.client.request("get_session_stats"); }
879
+ async validatePolicy() {
880
+ if (!this.policy)
881
+ return;
882
+ const state = await this.client.request("get_state");
883
+ const effectiveModel = state.model?.provider && state.model.id ? `${state.model.provider}/${state.model.id}` : null;
884
+ if (effectiveModel !== this.policy.model || state.model?.contextWindow !== this.policy.contextWindow) {
885
+ throw new Error("Pi model or context window changed after startup; compaction policy is no longer safe");
886
+ }
887
+ const ownedSettings = readOwnedPiSettings(this.policy.ownedPiDirectory);
888
+ assertEffectivePiCompactionSettings({ contextWindow: state.model?.contextWindow, compaction: {
889
+ enabled: state.autoCompactionEnabled,
890
+ reserveTokens: ownedSettings.compaction?.reserveTokens,
891
+ keepRecentTokens: ownedSettings.compaction?.keepRecentTokens,
892
+ } });
893
+ }
894
+ getState() { return this.client.request("get_state"); }
790
895
  dispose() { return this.client.close(); }
791
896
  subscribe(listener) {
792
897
  const offEvent = this.client.subscribe(listener);
@@ -809,8 +914,15 @@ export function resolvePiProcessExtensionArgs(input, resolvers = {}) {
809
914
  args.push("-e", bashTimeout);
810
915
  return args;
811
916
  }
812
- async function createPiRpcBackend(input, dependencies, spawn) {
917
+ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn = true) {
813
918
  const stateRoot = input.stateDir ?? path.join(input.workspaceDir, ".larkin");
919
+ const mergedEnv = { ...globalThis.process.env, ...dependencies.env, ...input.env, NO_COLOR: "1" };
920
+ assertNoProjectPiCompactionOverride(input.workspaceDir);
921
+ const ownedPiDirectory = mergedEnv.LARKIN_CONFIG_DIR
922
+ ? piAgentDirectory(mergedEnv.LARKIN_CONFIG_DIR, input.agentId)
923
+ : path.join(stateRoot, "pi-agent");
924
+ prepareOwnedPiDirectory(ownedPiDirectory);
925
+ traceProcessBoundary(mergedEnv, "pi-rpc:child-env", { configDir: mergedEnv.LARKIN_CONFIG_DIR, agentId: input.agentId, targetDir: ownedPiDirectory, childEnvConfigDir: mergedEnv.LARKIN_CONFIG_DIR || null, childEnvHome: mergedEnv.HOME || null });
814
926
  const runtimeDir = path.join(stateRoot, "runtime");
815
927
  fs.mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
816
928
  const promptFile = path.join(runtimeDir, "pi-standing-prompt.md");
@@ -824,28 +936,43 @@ async function createPiRpcBackend(input, dependencies, spawn) {
824
936
  ...(session.sessionFile ? ["--session", session.sessionFile] : []),
825
937
  ...(requestedModel ? ["--model", requestedModel] : []),
826
938
  ...(requestedEffort ? ["--thinking", requestedEffort] : [])];
827
- const mergedEnv = { ...globalThis.process.env, ...dependencies.env, ...input.env, NO_COLOR: "1" };
828
939
  const builtin = mergedEnv.LARKIN_PI_DISTRIBUTION === "builtin";
829
940
  const builtinSpec = builtin ? internalCommandSpec("pi-rpc", [], mergedEnv) : null;
830
941
  const command = builtinSpec?.command ?? dependencies.piCommand ?? dependencies.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
831
- const commandArgs = [...(builtinSpec?.args ?? []), ...args];
832
- if (builtin) {
833
- if (!mergedEnv.LARKIN_CONFIG_DIR)
834
- throw new Error("内置 Pi 缺少 LARKIN_CONFIG_DIR");
835
- mergedEnv.PI_CODING_AGENT_DIR = piAgentDirectory(mergedEnv.LARKIN_CONFIG_DIR, input.agentId);
942
+ const commandPrefix = builtinSpec?.args ?? dependencies.piCommandArgs ?? [];
943
+ const commandArgs = [...commandPrefix, ...args];
944
+ const reportedVersion = productionSpawn && !builtin
945
+ ? parsePiExecutableVersion(String(spawnSync(command, [...commandPrefix, "--version"], {
946
+ cwd: input.workspaceDir, env: mergedEnv, encoding: "utf8", timeout: 5_000,
947
+ }).stdout || ""))
948
+ : BUNDLED_PI_VERSION;
949
+ mergedEnv.PI_CODING_AGENT_DIR = ownedPiDirectory;
950
+ if (builtin)
836
951
  mergedEnv.PI_TELEMETRY = "0";
837
- }
838
- commandArgs.push(...(dependencies.resolvePiProcessExtensionArgs ?? resolvePiProcessExtensionArgs)({
952
+ const extensionArgs = (dependencies.resolvePiProcessExtensionArgs ?? resolvePiProcessExtensionArgs)({
839
953
  distribution: builtin ? "builtin" : "external",
840
954
  piCommand: command,
841
955
  env: mergedEnv,
842
956
  platform: process.platform,
843
- }));
957
+ });
958
+ commandArgs.push(...extensionArgs);
959
+ let probedModel = null;
960
+ if (productionSpawn) {
961
+ // Pi reads compaction settings when the process starts. Probe the exact
962
+ // requested model first, then write the matching 15% reserve before the
963
+ // real session starts; no prompt, provider request, package, or extension
964
+ // is loaded by the probe.
965
+ probedModel = await discoverEffectivePiContextWindow(input, command, commandPrefix, requestedModel, mergedEnv, spawn, dependencies.piRpcClientOptions);
966
+ writeOwnedPiSettings(ownedPiDirectory, calculatePiCompactionSettings(probedModel.contextWindow));
967
+ }
968
+ const artifactEntriesBeforeSpawn = new Set(fs.readdirSync(ownedPiDirectory));
969
+ const artifactSpawnBoundary = Date.now();
844
970
  const child = spawn(command, commandArgs, {
845
971
  cwd: input.workspaceDir,
846
972
  env: mergedEnv,
847
973
  stdio: ["pipe", "pipe", "pipe"],
848
974
  });
975
+ traceProcessBoundary(mergedEnv, "pi-rpc:child-spawned", { configDir: mergedEnv.LARKIN_CONFIG_DIR, agentId: input.agentId, targetDir: ownedPiDirectory, childPid: child.pid ?? null });
849
976
  const client = new PiRpcClient(child, dependencies.piRpcClientOptions);
850
977
  try {
851
978
  const [state, available] = await Promise.all([
@@ -855,6 +982,24 @@ async function createPiRpcBackend(input, dependencies, spawn) {
855
982
  if (!available?.models?.length)
856
983
  throw new Error("Pi has no authenticated available models. Run the official `pi` login flow or configure provider credentials; Larkin will not create a fallback session.");
857
984
  const effectiveModel = state.model?.provider && state.model.id ? `${state.model.provider}/${state.model.id}` : null;
985
+ if (productionSpawn) {
986
+ if (!effectiveModel || !probedModel || effectiveModel !== probedModel.model
987
+ || state.model?.contextWindow !== probedModel.contextWindow) {
988
+ throw new Error("Pi model or context window changed between the isolated probe and runtime startup");
989
+ }
990
+ const ownedSettings = readOwnedPiSettings(ownedPiDirectory);
991
+ assertEffectivePiCompactionSettings({ contextWindow: state.model?.contextWindow, compaction: {
992
+ enabled: state.autoCompactionEnabled,
993
+ reserveTokens: ownedSettings.compaction?.reserveTokens,
994
+ keepRecentTokens: ownedSettings.compaction?.keepRecentTokens,
995
+ } });
996
+ const handshake = state.compactionCapabilities;
997
+ verifyPiCapabilities({ distribution: builtin ? "builtin" : "external", version: reportedVersion,
998
+ contextWindow: state.model?.contextWindow, autoCompactionEnabled: state.autoCompactionEnabled,
999
+ reserveTokens: handshake?.reserveTokens, keepRecentTokens: handshake?.keepRecentTokens,
1000
+ compactRpc: true, events: handshake?.events,
1001
+ trustedProtocol: builtin });
1002
+ }
858
1003
  // 严格对账(Owner 决策):模型必须来自 pi 的权威可用列表,不做后缀猜测——
859
1004
  // 配置的模型与 pi 可用列表不符即报错,并在错误里列出可用模型。
860
1005
  const availableEntries = (available.models ?? []).map((entry) => ({
@@ -867,7 +1012,10 @@ async function createPiRpcBackend(input, dependencies, spawn) {
867
1012
  }
868
1013
  if (requestedEffort && state.thinkingLevel !== requestedEffort)
869
1014
  throw new Error(`Pi thinking level ${requestedEffort} was not accepted by effective model ${effectiveModel || "unknown"}`);
870
- return new PiRpcBackend(client, state);
1015
+ recordPiRuntimeArtifactProvenance(ownedPiDirectory, artifactEntriesBeforeSpawn, artifactSpawnBoundary);
1016
+ return new PiRpcBackend(client, state, productionSpawn ? {
1017
+ model: effectiveModel, contextWindow: state.model?.contextWindow, ownedPiDirectory,
1018
+ } : undefined);
871
1019
  }
872
1020
  catch (error) {
873
1021
  await client.close();
@@ -972,7 +1120,8 @@ export function createNativeRuntimeAdapter(id, dependencies = {}) {
972
1120
  capabilities: CAPABILITIES[id],
973
1121
  async probe(input) {
974
1122
  const readiness = await probeNativeRuntimeReadiness({ runtime: id, agentId: input.agentId, cwd: input.workspaceDir,
975
- env: { ...dependencies.env, ...input.env }, command: configuredCommand });
1123
+ env: { ...dependencies.env, ...input.env }, command: configuredCommand,
1124
+ ...(id === "pi" && dependencies.piCommandArgs ? { commandArgs: dependencies.piCommandArgs } : {}) });
976
1125
  resolvedExecutable = readiness.state === "ready" ? readiness.executable ?? null : null;
977
1126
  return readiness;
978
1127
  },
@@ -988,7 +1137,7 @@ export function createNativeRuntimeAdapter(id, dependencies = {}) {
988
1137
  ? "builtin" : "external";
989
1138
  return new PiSession(await (dependencies.createPiSession
990
1139
  ? dependencies.createPiSession(input)
991
- : createPiRpcBackend(input, { ...dependencies, piCommand: resolvedExecutable }, spawn)), distribution);
1140
+ : createPiRpcBackend(input, { ...dependencies, piCommand: resolvedExecutable }, spawn, productionSpawn)), distribution);
992
1141
  }
993
1142
  if (id === "codex") {
994
1143
  const codexInput = dependencies.codexModelOverride?.trim()