@abelxiaoxing/cadence 2.0.2 → 2.0.4

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.
@@ -26,10 +26,14 @@ export function refused(): never {
26
26
  export function hash(bytes: string | Buffer): string {
27
27
  return createHash("sha256").update(bytes).digest("hex");
28
28
  }
29
- function privateMode(st: Stats): void {
29
+ /** Owner-only mode for state Cadence creates. External sources it only reads
30
+ * (Pi's agent directory and files) keep whatever mode the user's umask gave
31
+ * them, such as 0755/0775 from Pi itself; ownership is still required. */
32
+ function privateMode(st: Stats, ownerOnly = true): void {
30
33
  if (
31
34
  process.platform !== "win32" &&
32
- ((st.mode & 0o077) !== 0 || (process.getuid && st.uid !== process.getuid()))
35
+ ((ownerOnly && (st.mode & 0o077) !== 0) ||
36
+ (process.getuid && st.uid !== process.getuid()))
33
37
  )
34
38
  refused();
35
39
  }
@@ -80,7 +84,7 @@ export class DirectoryWalks {
80
84
  remember(path: string, st: Stats): void {
81
85
  if (this.open()) this.walked.set(path, { dev: st.dev, ino: st.ino });
82
86
  }
83
- reuse(path: string): boolean {
87
+ reuse(path: string, ownerOnly = true): boolean {
84
88
  const walked = this.open() ? this.walked.get(path) : undefined;
85
89
  if (!walked) return false;
86
90
  let st: Stats;
@@ -100,7 +104,7 @@ export class DirectoryWalks {
100
104
  this.walked.delete(path);
101
105
  return false;
102
106
  }
103
- privateMode(st);
107
+ privateMode(st, ownerOnly);
104
108
  return true;
105
109
  }
106
110
  }
@@ -109,14 +113,18 @@ export class DirectoryWalks {
109
113
  * or hostile-same-user guarantee; failed temporaries are deliberately preserved.
110
114
  */
111
115
  export class PrivateFiles {
112
- constructor(readonly namespace: string) {}
116
+ /** `ownerOnly: false` for an external namespace such as Pi's agent directory. */
117
+ constructor(
118
+ readonly namespace: string,
119
+ readonly ownerOnly = true,
120
+ ) {}
113
121
  directory(path: string, create = false, walks?: DirectoryWalks): boolean {
114
122
  const tail = relative(this.namespace, path);
115
123
  if (isAbsolute(tail) || tail === ".." || tail.startsWith(`..${sep}`))
116
124
  refused();
117
125
  if (!create) {
118
126
  const full = resolve(path);
119
- if (walks?.reuse(full)) return true;
127
+ if (walks?.reuse(full, this.ownerOnly)) return true;
120
128
  const namespace = resolve(this.namespace);
121
129
  let inside = false,
122
130
  last: Stats | undefined;
@@ -126,7 +134,7 @@ export class PrivateFiles {
126
134
  // including an unsafe parent before a missing child. Only the caller's
127
135
  // DirectoryWalks keeps the result, for that same synchronous operation.
128
136
  if (at === namespace) inside = true;
129
- if (inside) privateMode(st);
137
+ if (inside) privateMode(st, this.ownerOnly);
130
138
  last = st;
131
139
  });
132
140
  if (found && last) walks?.remember(full, last);
@@ -135,7 +143,7 @@ export class PrivateFiles {
135
143
  // Walk the namespace and its ambient ancestors once, then check each
136
144
  // private segment exactly once, creating a missing one privately.
137
145
  if (!safeDirectory(this.namespace, true)) return false;
138
- privateMode(lstatSync(this.namespace));
146
+ privateMode(lstatSync(this.namespace), this.ownerOnly);
139
147
  let at = this.namespace;
140
148
  for (const part of tail.split(sep).filter(Boolean)) {
141
149
  at = join(at, part);
@@ -160,7 +168,7 @@ export class PrivateFiles {
160
168
  }
161
169
  if (!st.isDirectory() || st.isSymbolicLink())
162
170
  throw new Error("Unsafe directory");
163
- privateMode(st);
171
+ privateMode(st, this.ownerOnly);
164
172
  }
165
173
  return true;
166
174
  }
@@ -179,7 +187,7 @@ export class PrivateFiles {
179
187
  }
180
188
  if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
181
189
  refused();
182
- privateMode(before);
190
+ privateMode(before, this.ownerOnly);
183
191
  const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
184
192
  try {
185
193
  const opened = fstatSync(fd);
@@ -564,7 +564,8 @@ var thinkingLevels = [
564
564
  "low",
565
565
  "medium",
566
566
  "high",
567
- "xhigh"
567
+ "xhigh",
568
+ "max"
568
569
  ];
569
570
  function decodeModelSelector(v) {
570
571
  const o = closed(v, ["provider", "model", "api", "thinking"]);
@@ -2510,6 +2511,23 @@ function hostSystem(root) {
2510
2511
  "--value"
2511
2512
  ]);
2512
2513
  return r.status === 0 ? r.stdout.trim() === "yes" : undefined;
2514
+ },
2515
+ enableLinger: (interactive) => {
2516
+ const r = interactive ? spawnSync2("loginctl", ["enable-linger"], { stdio: "inherit" }) : spawnSync2("busctl", [
2517
+ "--system",
2518
+ "--allow-interactive-authorization=no",
2519
+ "--timeout=15",
2520
+ "call",
2521
+ "org.freedesktop.login1",
2522
+ "/org/freedesktop/login1",
2523
+ "org.freedesktop.login1.Manager",
2524
+ "SetUserLinger",
2525
+ "ubb",
2526
+ String(process.getuid?.()),
2527
+ "true",
2528
+ "false"
2529
+ ], { stdio: "ignore", timeout: 20000 });
2530
+ return !r.error && r.status === 0;
2513
2531
  }
2514
2532
  };
2515
2533
  }
@@ -2672,7 +2690,17 @@ function regularFile(path) {
2672
2690
  return false;
2673
2691
  }
2674
2692
  }
2675
- function configureAutostart(s, mode, requested) {
2693
+ function bootStart(s, attempt, interactive) {
2694
+ const current = s.linger();
2695
+ if (current === undefined)
2696
+ return {};
2697
+ if (current)
2698
+ return { linger: "on" };
2699
+ if (!attempt)
2700
+ return { linger: "off" };
2701
+ return { linger: s.enableLinger(interactive) ? "enabled" : "refused" };
2702
+ }
2703
+ function configureAutostart(s, mode, requested, linger = false) {
2676
2704
  if (s.platform !== "linux")
2677
2705
  return { status: "skipped" };
2678
2706
  if (mode === "refresh" && (s.env.CADENCE_AUTOSTART === "0" || !piUserInstall(s)))
@@ -2714,11 +2742,12 @@ function configureAutostart(s, mode, requested) {
2714
2742
  return { status: "failed", ...result };
2715
2743
  if (!wanted && !active)
2716
2744
  return { status: "disabled", ...result };
2745
+ const boot = () => wanted ? bootStart(s, mode === "install" ? linger : previous === undefined && s.env.CADENCE_LINGER !== "0", mode === "install") : {};
2717
2746
  if (!regularFile(join2(agentDir, "auth.json")))
2718
- return { status: "waiting-for-pi-login", ...result };
2747
+ return { status: "waiting-for-pi-login", ...result, ...boot() };
2719
2748
  s.systemctl(["reset-failed", s.unit]);
2720
2749
  const started = s.systemctl(["start", s.unit]).status === 0;
2721
- return { status: started ? "started" : "failed", ...result };
2750
+ return { status: started ? "started" : "failed", ...result, ...boot() };
2722
2751
  }
2723
2752
  function removeAutostart(s) {
2724
2753
  if (s.platform !== "linux")
@@ -2751,7 +2780,8 @@ function autostartStatus(s) {
2751
2780
  status: s.systemctl(["is-active", s.unit]).stdout.trim() || "unknown",
2752
2781
  enabled: s.systemctl(["is-enabled", s.unit]).stdout.trim() || "unknown",
2753
2782
  unit: path,
2754
- workspace: generatedWorkspace(previous)
2783
+ workspace: generatedWorkspace(previous),
2784
+ ...bootStart(s, false, false)
2755
2785
  };
2756
2786
  }
2757
2787
 
@@ -2786,8 +2816,8 @@ function refused() {
2786
2816
  function hash(bytes) {
2787
2817
  return createHash2("sha256").update(bytes).digest("hex");
2788
2818
  }
2789
- function privateMode(st) {
2790
- if (process.platform !== "win32" && ((st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
2819
+ function privateMode(st, ownerOnly = true) {
2820
+ if (process.platform !== "win32" && (ownerOnly && (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
2791
2821
  refused();
2792
2822
  }
2793
2823
  function same(a, b) {
@@ -2822,7 +2852,7 @@ class DirectoryWalks {
2822
2852
  if (this.open())
2823
2853
  this.walked.set(path, { dev: st.dev, ino: st.ino });
2824
2854
  }
2825
- reuse(path) {
2855
+ reuse(path, ownerOnly = true) {
2826
2856
  const walked = this.open() ? this.walked.get(path) : undefined;
2827
2857
  if (!walked)
2828
2858
  return false;
@@ -2839,15 +2869,17 @@ class DirectoryWalks {
2839
2869
  this.walked.delete(path);
2840
2870
  return false;
2841
2871
  }
2842
- privateMode(st);
2872
+ privateMode(st, ownerOnly);
2843
2873
  return true;
2844
2874
  }
2845
2875
  }
2846
2876
 
2847
2877
  class PrivateFiles {
2848
2878
  namespace;
2849
- constructor(namespace) {
2879
+ ownerOnly;
2880
+ constructor(namespace, ownerOnly = true) {
2850
2881
  this.namespace = namespace;
2882
+ this.ownerOnly = ownerOnly;
2851
2883
  }
2852
2884
  directory(path, create = false, walks) {
2853
2885
  const tail = relative2(this.namespace, path);
@@ -2855,7 +2887,7 @@ class PrivateFiles {
2855
2887
  refused();
2856
2888
  if (!create) {
2857
2889
  const full = resolve3(path);
2858
- if (walks?.reuse(full))
2890
+ if (walks?.reuse(full, this.ownerOnly))
2859
2891
  return true;
2860
2892
  const namespace = resolve3(this.namespace);
2861
2893
  let inside = false, last;
@@ -2863,7 +2895,7 @@ class PrivateFiles {
2863
2895
  if (at2 === namespace)
2864
2896
  inside = true;
2865
2897
  if (inside)
2866
- privateMode(st);
2898
+ privateMode(st, this.ownerOnly);
2867
2899
  last = st;
2868
2900
  });
2869
2901
  if (found && last)
@@ -2872,7 +2904,7 @@ class PrivateFiles {
2872
2904
  }
2873
2905
  if (!safeDirectory(this.namespace, true))
2874
2906
  return false;
2875
- privateMode(lstatSync3(this.namespace));
2907
+ privateMode(lstatSync3(this.namespace), this.ownerOnly);
2876
2908
  let at = this.namespace;
2877
2909
  for (const part of tail.split(sep3).filter(Boolean)) {
2878
2910
  at = join3(at, part);
@@ -2892,7 +2924,7 @@ class PrivateFiles {
2892
2924
  }
2893
2925
  if (!st.isDirectory() || st.isSymbolicLink())
2894
2926
  throw new Error("Unsafe directory");
2895
- privateMode(st);
2927
+ privateMode(st, this.ownerOnly);
2896
2928
  }
2897
2929
  return true;
2898
2930
  }
@@ -2909,7 +2941,7 @@ class PrivateFiles {
2909
2941
  }
2910
2942
  if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
2911
2943
  refused();
2912
- privateMode(before);
2944
+ privateMode(before, this.ownerOnly);
2913
2945
  const fd = openSync2(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
2914
2946
  try {
2915
2947
  const opened = fstatSync2(fd);
@@ -5843,13 +5875,13 @@ function startupGuidance(result, input) {
5843
5875
  return `Cadence refused its installed package files (web/dist or agents); reinstall the package rather than editing it.
5844
5876
  `;
5845
5877
  case "pi-agent-directory":
5846
- return `Pi's agent directory ${agent} must be an existing real directory owned by you with mode 0700 (chmod 700).
5878
+ return `Pi's agent directory ${agent} must be an existing real directory (not a symlink) owned by you.
5847
5879
  `;
5848
5880
  case "pi-auth":
5849
- return `${join8(agent, "auth.json")} must be your own single-link regular file with mode 0600 holding valid Pi credentials; log in to Pi first.
5881
+ return `${join8(agent, "auth.json")} must be your own single-link regular file holding valid Pi credentials; log in to Pi first.
5850
5882
  `;
5851
5883
  case "pi-models":
5852
- return `${join8(agent, "models.json")} must be your own single-link regular file with mode 0600, using only fields Cadence's pinned Pi SDK supports; a field only newer Pi understands is refused.
5884
+ return `${join8(agent, "models.json")} must be your own single-link regular file using only fields Cadence's pinned Pi SDK supports; a field only newer Pi understands is refused.
5853
5885
  `;
5854
5886
  case "pi-runtime":
5855
5887
  return `Loading Pi's SDK model runtime failed after the owner registered (optional SDK packages missing, or sources the SDK refuses); it stays quarantined.
@@ -5984,15 +6016,30 @@ function autostartGuidance(s, result) {
5984
6016
  ` : "";
5985
6017
  return note + autostartStatusGuidance(s, result, name, cli, workspace, manage);
5986
6018
  }
6019
+ function lingerGuidance(linger) {
6020
+ switch (linger) {
6021
+ case "enabled":
6022
+ return `Linger enabled: Cadence starts at boot and keeps running after logout (undo: loginctl disable-linger).
6023
+ `;
6024
+ case "off":
6025
+ return `Linger is off: Cadence starts at login and stops at logout. Start it at boot: loginctl enable-linger
6026
+ `;
6027
+ case "refused":
6028
+ return `Linger needs authorization: Cadence starts at login and stops at logout. Start it at boot: loginctl enable-linger (may ask for your password)
6029
+ `;
6030
+ default:
6031
+ return "";
6032
+ }
6033
+ }
5987
6034
  function autostartStatusGuidance(s, result, name, cli, workspace, manage) {
5988
6035
  switch (result.status) {
5989
6036
  case "started": {
6037
+ const linger = lingerGuidance(result.linger);
5990
6038
  if (result.service !== "alive")
5991
6039
  return `${s.unit} started for ${result.workspace}, but the owner reports ${result.service}.
5992
6040
  Inspect: node ${cli} diagnose --workspace ${workspace}
5993
6041
  ${manage}
5994
- `;
5995
- const linger = s.linger() === false ? "Linger is off: Cadence starts at login and stops at logout. `loginctl enable-linger` keeps it running and starts it at boot.\n" : "";
6042
+ ${linger}`;
5996
6043
  return `Cadence WebUI: ${result.endpoint} (${s.unit}, workspace ${result.workspace})
5997
6044
  Enter with your private link: node ${cli} token-link --workspace ${workspace} --reveal
5998
6045
  ${manage}
@@ -6001,7 +6048,7 @@ ${linger}`;
6001
6048
  case "waiting-for-pi-login":
6002
6049
  return `${s.unit} is enabled for ${result.workspace}; it starts once Pi has ${join8(agentDirectory(s), "auth.json")}.
6003
6050
  Log in to Pi, then: systemctl --user start ${name}
6004
- `;
6051
+ ${lingerGuidance(result.linger)}`;
6005
6052
  case "disabled":
6006
6053
  return `${s.unit} refreshed for ${result.workspace}; left disabled as configured.
6007
6054
  `;
@@ -6019,7 +6066,7 @@ Log in to Pi, then: systemctl --user start ${name}
6019
6066
  `;
6020
6067
  case "failed":
6021
6068
  return `Could not enable or start ${s.unit}; see systemctl --user status ${name} and journalctl --user -u ${name}.
6022
- `;
6069
+ ${lingerGuidance(result.linger)}`;
6023
6070
  default:
6024
6071
  return "";
6025
6072
  }
@@ -6027,8 +6074,16 @@ Log in to Pi, then: systemctl --user start ${name}
6027
6074
  async function autostartMain(argv, hostUrl) {
6028
6075
  const action = argv.shift();
6029
6076
  let requested;
6030
- if (action === "install" && argv[0] === "--workspace" && argv[1])
6031
- requested = resolve5(argv.splice(0, 2)[1]);
6077
+ let linger = false;
6078
+ while (action === "install" && argv.length) {
6079
+ if (argv[0] === "--workspace" && argv[1] && requested === undefined)
6080
+ requested = resolve5(argv.splice(0, 2)[1]);
6081
+ else if (argv[0] === "--linger" && !linger) {
6082
+ argv.shift();
6083
+ linger = true;
6084
+ } else
6085
+ break;
6086
+ }
6032
6087
  if (argv.length || !["install", "refresh", "remove", "status"].includes(action ?? ""))
6033
6088
  throw new ContractError("invalid");
6034
6089
  const s = hostSystem(dirname8(dirname8(fileURLToPath(hostUrl))));
@@ -6039,7 +6094,7 @@ async function autostartMain(argv, hostUrl) {
6039
6094
  result = autostartStatus(s);
6040
6095
  else {
6041
6096
  try {
6042
- result = configureAutostart(s, action === "install" ? "install" : "refresh", requested);
6097
+ result = configureAutostart(s, action === "install" ? "install" : "refresh", requested, linger);
6043
6098
  } catch (error) {
6044
6099
  if (error instanceof ContractError && error.code === "invalid")
6045
6100
  process.stderr.write(`Cadence autostart needs an existing absolute workspace directory (--workspace or CADENCE_WORKSPACE).
@@ -6062,8 +6117,8 @@ async function managementMain(argv, hostUrl) {
6062
6117
  const command = argv.shift();
6063
6118
  if (command === "--help" && !argv.length) {
6064
6119
  process.stdout.write(`cadence-service start|status|diagnose|stop|token-link --reveal|token-rotate|recover inspect|recover resolve [--workspace PATH] [--state-home PATH]
6065
- cadence-service autostart install [--workspace PATH]|refresh|remove|status
6066
- Start is explicit, or the Linux systemd user unit that Pi's user-scope install registers (CADENCE_AUTOSTART=0, disable or mask opts out). Default detached or --foreground. Start accepts --config, --agent-dir, --host, --port, --token-file.
6120
+ cadence-service autostart install [--workspace PATH] [--linger]|refresh|remove|status
6121
+ Start is explicit, or the Linux systemd user unit that Pi's user-scope install registers (CADENCE_AUTOSTART=0, disable or mask opts out); its first registration also enables linger for boot start when logind allows it without a password (CADENCE_LINGER=0 opts out), and --linger asks for it explicitly. Default detached or --foreground. Start accepts --config, --agent-dir, --host, --port, --token-file.
6067
6122
  Return/quit detaches clients; only explicit Cancel/Stop requests stop. Unconfirmed/quarantined is not stopped.
6068
6123
  Recovery resolve requires this local native terminal, exact unchanged dead-owner resources and separate process/credential/storage acknowledgments. No --yes, network recovery, killing, repair or cleanup.
6069
6124
  `);
@@ -951,7 +951,8 @@ var thinkingLevels = [
951
951
  "low",
952
952
  "medium",
953
953
  "high",
954
- "xhigh"
954
+ "xhigh",
955
+ "max"
955
956
  ];
956
957
  function decodeModelSelector(v) {
957
958
  const o = closed(v, ["provider", "model", "api", "thinking"]);
@@ -2134,8 +2135,8 @@ function refused() {
2134
2135
  function hash(bytes) {
2135
2136
  return createHash3("sha256").update(bytes).digest("hex");
2136
2137
  }
2137
- function privateMode(st) {
2138
- if (process.platform !== "win32" && ((st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
2138
+ function privateMode(st, ownerOnly = true) {
2139
+ if (process.platform !== "win32" && (ownerOnly && (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
2139
2140
  refused();
2140
2141
  }
2141
2142
  function same(a, b) {
@@ -2170,7 +2171,7 @@ class DirectoryWalks {
2170
2171
  if (this.open())
2171
2172
  this.walked.set(path, { dev: st.dev, ino: st.ino });
2172
2173
  }
2173
- reuse(path) {
2174
+ reuse(path, ownerOnly = true) {
2174
2175
  const walked = this.open() ? this.walked.get(path) : undefined;
2175
2176
  if (!walked)
2176
2177
  return false;
@@ -2187,15 +2188,17 @@ class DirectoryWalks {
2187
2188
  this.walked.delete(path);
2188
2189
  return false;
2189
2190
  }
2190
- privateMode(st);
2191
+ privateMode(st, ownerOnly);
2191
2192
  return true;
2192
2193
  }
2193
2194
  }
2194
2195
 
2195
2196
  class PrivateFiles {
2196
2197
  namespace;
2197
- constructor(namespace) {
2198
+ ownerOnly;
2199
+ constructor(namespace, ownerOnly = true) {
2198
2200
  this.namespace = namespace;
2201
+ this.ownerOnly = ownerOnly;
2199
2202
  }
2200
2203
  directory(path, create = false, walks) {
2201
2204
  const tail = relative2(this.namespace, path);
@@ -2203,7 +2206,7 @@ class PrivateFiles {
2203
2206
  refused();
2204
2207
  if (!create) {
2205
2208
  const full = resolve2(path);
2206
- if (walks?.reuse(full))
2209
+ if (walks?.reuse(full, this.ownerOnly))
2207
2210
  return true;
2208
2211
  const namespace = resolve2(this.namespace);
2209
2212
  let inside = false, last;
@@ -2211,7 +2214,7 @@ class PrivateFiles {
2211
2214
  if (at2 === namespace)
2212
2215
  inside = true;
2213
2216
  if (inside)
2214
- privateMode(st);
2217
+ privateMode(st, this.ownerOnly);
2215
2218
  last = st;
2216
2219
  });
2217
2220
  if (found && last)
@@ -2220,7 +2223,7 @@ class PrivateFiles {
2220
2223
  }
2221
2224
  if (!safeDirectory(this.namespace, true))
2222
2225
  return false;
2223
- privateMode(lstatSync2(this.namespace));
2226
+ privateMode(lstatSync2(this.namespace), this.ownerOnly);
2224
2227
  let at = this.namespace;
2225
2228
  for (const part of tail.split(sep2).filter(Boolean)) {
2226
2229
  at = join2(at, part);
@@ -2240,7 +2243,7 @@ class PrivateFiles {
2240
2243
  }
2241
2244
  if (!st.isDirectory() || st.isSymbolicLink())
2242
2245
  throw new Error("Unsafe directory");
2243
- privateMode(st);
2246
+ privateMode(st, this.ownerOnly);
2244
2247
  }
2245
2248
  return true;
2246
2249
  }
@@ -2257,7 +2260,7 @@ class PrivateFiles {
2257
2260
  }
2258
2261
  if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
2259
2262
  refused();
2260
- privateMode(before);
2263
+ privateMode(before, this.ownerOnly);
2261
2264
  const fd = openSync2(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
2262
2265
  try {
2263
2266
  const opened = fstatSync2(fd);
@@ -4000,7 +4003,7 @@ function decodeModelCandidate(value) {
4000
4003
  thinking: list(o.thinking, 7, (v) => {
4001
4004
  const t = closed(v, ["level", "selectable"]);
4002
4005
  return {
4003
- level: oneOf(t.level, [...thinkingLevels, "max"]),
4006
+ level: oneOf(t.level, thinkingLevels),
4004
4007
  selectable: bool(t.selectable)
4005
4008
  };
4006
4009
  }),
@@ -4010,7 +4013,7 @@ function decodeModelCandidate(value) {
4010
4013
  "auth-unavailable"
4011
4014
  ])
4012
4015
  };
4013
- if (new Set(candidate.thinking.map((t) => t.level)).size !== candidate.thinking.length || candidate.thinking.some((t) => t.selectable && (t.level === "max" || candidate.api === null || candidate.eligibility !== "candidate")) || candidate.eligibility === "candidate" && !candidate.thinking.some((t) => t.selectable))
4016
+ if (new Set(candidate.thinking.map((t) => t.level)).size !== candidate.thinking.length || candidate.thinking.some((t) => t.selectable && (candidate.api === null || candidate.eligibility !== "candidate")) || candidate.eligibility === "candidate" && !candidate.thinking.some((t) => t.selectable))
4014
4017
  invalid();
4015
4018
  return candidate;
4016
4019
  }
@@ -17108,7 +17111,7 @@ function piSource(agentDir, home, cwd, env) {
17108
17111
  const directory = join12(realpathSync4(dirname9(selected2)), selected2.slice(dirname9(selected2).length + 1));
17109
17112
  if (!lstatSync8(directory).isDirectory() || lstatSync8(directory).isSymbolicLink())
17110
17113
  refuse2();
17111
- const files = new PrivateFiles(directory);
17114
+ const files = new PrivateFiles(directory, false);
17112
17115
  if (!files.directory(directory))
17113
17116
  refuse2();
17114
17117
  return {
@@ -22606,7 +22609,7 @@ function refuseAt(stage, step) {
22606
22609
  }
22607
22610
  }
22608
22611
  async function createServiceHost(value, sourceBuild) {
22609
- const embedded = "e7e5248f8273a373827ac70d2d8be1d5aaea1e329379e59e4947fc10f82be818";
22612
+ const embedded = "ff017feffc6b6b4521e470d82514d0ca008e15472fa1442ec498b243b0f7cf87";
22610
22613
  if (embedded && sourceBuild)
22611
22614
  throw new ContractError("unsupported-context");
22612
22615
  const contractHash = decodeHash(embedded ?? sourceBuild?.contractHash);
package/src/task-shell.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  } from "@earendil-works/pi-tui";
12
12
  import {
13
13
  autostartHint,
14
+ departure,
14
15
  MANAGEMENT_GUIDANCE,
15
16
  NativeConnection,
16
17
  } from "./native-client.ts";
@@ -98,10 +99,10 @@ export function registerTaskShell(pi: ExtensionAPI): void {
98
99
  dismiss?.();
99
100
  // Detach original attachment, not service jobs/provider/auth. Shutdown does
100
101
  // not wait for their lifetime. Retained transport blocks uncertain reuse.
101
- void connection.detach().catch(() => {});
102
+ return connection.detach().catch(() => {});
102
103
  };
103
104
  pi.on("session_start", (_event, ctx) => {
104
- depart();
105
+ void depart();
105
106
  unsubscribe?.();
106
107
  unsubscribe = undefined;
107
108
  selectedTask = undefined;
@@ -112,15 +113,17 @@ export function registerTaskShell(pi: ExtensionAPI): void {
112
113
  });
113
114
  });
114
115
  pi.on("session_shutdown", () => {
115
- depart();
116
+ const leaving = depart();
116
117
  unsubscribe?.();
117
118
  unsubscribe = undefined;
119
+ return departure(leaving);
118
120
  });
119
- pi.on("session_before_switch", depart);
120
- pi.on("session_before_fork", depart);
121
- pi.on("session_before_tree", depart);
122
- pi.on("session_before_compact", depart);
123
- pi.on("model_select", depart);
121
+ const leave = () => void depart();
122
+ pi.on("session_before_switch", leave);
123
+ pi.on("session_before_fork", leave);
124
+ pi.on("session_before_tree", leave);
125
+ pi.on("session_before_compact", leave);
126
+ pi.on("model_select", leave);
124
127
  // Only local dialog input is owned. Detached service work never owns Pi's
125
128
  // conversation/tool lifetime or silently cancels it.
126
129
  pi.on("input", () => (modal ? { action: "handled" as const } : undefined));
@@ -587,7 +590,7 @@ export function registerTaskShell(pi: ExtensionAPI): void {
587
590
  "Exact thinking level • no clamping/fallback",
588
591
  model.thinking.filter((t) => t.selectable).map((t) => t.level),
589
592
  );
590
- if (!level || level === "max") return;
593
+ if (!level) return;
591
594
  // Decode via the shared model-choice method; never inspect parent auth.
592
595
  const choice = {
593
596
  provider: model.provider,
@@ -411,6 +411,8 @@ export const modelApis = [
411
411
  "mistral-conversations",
412
412
  "azure-openai-responses",
413
413
  ];
414
+ // The pinned SDK's ThinkingLevel, including "max" (models whose
415
+ // thinkingLevelMap maps only max, such as some DeepSeek configurations).
414
416
  export const thinkingLevels = [
415
417
  "off",
416
418
  "minimal",
@@ -418,6 +420,7 @@ export const thinkingLevels = [
418
420
  "medium",
419
421
  "high",
420
422
  "xhigh",
423
+ "max",
421
424
  ];
422
425
  export function decodeModelSelector(v) {
423
426
  const o = closed(v, ["provider", "model", "api", "thinking"]);