@code-yeongyu/senpi 2026.11.10-2 → 2026.11.10-3

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 (117) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/bundle/chunks/{args-77GYOPEO.js → args-HFYB6URH.js} +1 -1
  3. package/dist/bundle/chunks/{azure-openai-responses-YFZ734T3.js → azure-openai-responses-MNN2TINI.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-ZUQCYIRY.js → chunk-2ODGHBUD.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-MSDBBOB2.js → chunk-5H43O4MA.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-IKJCA5SH.js → chunk-6Q7Y2G5A.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-YCPURLPU.js → chunk-AC6MDRAE.js} +64 -64
  8. package/dist/bundle/chunks/{chunk-33MZJTQ3.js → chunk-B4WD5XHP.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-V5FSENW6.js → chunk-COKX65TE.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-YNGEZBTP.js → chunk-DEQLHN2J.js} +2 -2
  11. package/dist/bundle/chunks/{chunk-EJB5E2JK.js → chunk-DHAAWL2H.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-2EJ4BGBM.js → chunk-DHORIIVQ.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-2M54EAAY.js → chunk-DIHIEJFV.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-RCHIMH7N.js → chunk-DQY26AX3.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-3Q5ZENSD.js → chunk-E6ZBTSC6.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-74IB3JPP.js → chunk-H3VJVKUK.js} +4 -4
  17. package/dist/bundle/chunks/{chunk-OF3XVGG4.js → chunk-HZDKQOWB.js} +1 -1
  18. package/dist/bundle/chunks/chunk-IREUIMFE.js +11 -0
  19. package/dist/bundle/chunks/{chunk-TUEZNIBK.js → chunk-JSD6O3RJ.js} +2 -2
  20. package/dist/bundle/chunks/{chunk-UOZDDYBN.js → chunk-L3KVP7VJ.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-7MN7YNO5.js → chunk-L7DGTA3L.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-J2NGJKXL.js → chunk-LXINMDMX.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-QATU6BBC.js → chunk-NN7NESW2.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-2U6HXWXS.js → chunk-NNQ34ZDW.js} +1 -1
  25. package/dist/bundle/chunks/chunk-NQKCJQHB.js +2 -0
  26. package/dist/bundle/chunks/{chunk-XH24X5B2.js → chunk-O5WJVIEU.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-O3FG6HML.js → chunk-PJI5GT7U.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-BPNSOZ2C.js → chunk-QUVSIW2R.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-WXYMAUFX.js → chunk-QX7WD3IM.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-Y4ONXQMR.js → chunk-RGHVPUUX.js} +1 -1
  31. package/dist/bundle/chunks/{chunk-RYPM5BZQ.js → chunk-S7NAJQB3.js} +1 -1
  32. package/dist/bundle/chunks/{chunk-LT353NSF.js → chunk-T4CUD7RF.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-J7FW7MED.js → chunk-TCZORL3V.js} +1 -1
  34. package/dist/bundle/chunks/{chunk-KU72IKV5.js → chunk-VGSWOLIY.js} +1 -1
  35. package/dist/bundle/chunks/{chunk-Q4G5DECT.js → chunk-Z2C5N2LM.js} +2 -2
  36. package/dist/bundle/chunks/{cli-main-Y6ALA5OI.js → cli-main-ZTT3N4DO.js} +1 -1
  37. package/dist/bundle/chunks/{credential-accounts-LVVPKFEY.js → credential-accounts-5KHVGSCY.js} +1 -1
  38. package/dist/bundle/chunks/{dispatch-EP35KKJ3.js → dispatch-YUYZQXRP.js} +1 -1
  39. package/dist/bundle/chunks/{hand-entry-IPDAIOGI.js → hand-entry-UV5XPMBF.js} +3 -3
  40. package/dist/bundle/chunks/{headless-host-HKGNJGYT.js → headless-host-53UMJMM6.js} +1 -1
  41. package/dist/bundle/chunks/{health-write-FBF74QJ7.js → health-write-JLSF5EVV.js} +1 -1
  42. package/dist/bundle/chunks/{help-fast-path-5SCPY3OH.js → help-fast-path-552QLMWT.js} +1 -1
  43. package/dist/bundle/chunks/{host-command-2326GCUP.js → host-command-BZN7WYYV.js} +1 -1
  44. package/dist/bundle/chunks/{host-session-command-BZYBDOIE.js → host-session-command-O5WYHZXH.js} +1 -1
  45. package/dist/bundle/chunks/{interactive-mode-LBQB7DDR.js → interactive-mode-OCU4NDPC.js} +1 -1
  46. package/dist/bundle/chunks/{login-health-EVXVLHB2.js → login-health-TOSQT5RG.js} +1 -1
  47. package/dist/bundle/chunks/{main-FFRFP34W.js → main-CNDP6HCF.js} +1 -1
  48. package/dist/bundle/chunks/{multi-session-host-NDIHHJ2L.js → multi-session-host-ODGJAGJP.js} +1 -1
  49. package/dist/bundle/chunks/openai-codex-responses-VRGOAFEG.js +10 -0
  50. package/dist/bundle/chunks/{openai-responses-NIVBWE2H.js → openai-responses-I7PXRWCM.js} +2 -2
  51. package/dist/bundle/chunks/{package-manager-cli-XCPAMOYI.js → package-manager-cli-HSPUG4RJ.js} +1 -1
  52. package/dist/bundle/chunks/{role-HQUN4XXK.js → role-AEOLJRTP.js} +1 -1
  53. package/dist/bundle/chunks/{rotation-stream-F3BEMXBE.js → rotation-stream-E3FJFHXJ.js} +1 -1
  54. package/dist/bundle/chunks/{rpc-mode-TKNZM4PZ.js → rpc-mode-KOZPXU3U.js} +1 -1
  55. package/dist/bundle/chunks/{run-attached-tui-JDUKXDI2.js → run-attached-tui-4AENRP7P.js} +1 -1
  56. package/dist/bundle/chunks/{schedule-command-HF2DHABB.js → schedule-command-SG44FE23.js} +1 -1
  57. package/dist/bundle/chunks/{session-control-endpoint-A6R6W2JQ.js → session-control-endpoint-5A24JYPE.js} +1 -1
  58. package/dist/bundle/chunks/{session-picker-GWA4S2PO.js → session-picker-73YRS3GE.js} +1 -1
  59. package/dist/bundle/chunks/{session-slot-CZGQQCOC.js → session-slot-RH7JLO3F.js} +1 -1
  60. package/dist/bundle/chunks/session-worker.js +139 -139
  61. package/dist/bundle/chunks/{state-store-S7QAESIF.js → state-store-IECVTYXC.js} +1 -1
  62. package/dist/bundle/cli.js +2 -2
  63. package/dist/bundle/index.js +1 -1
  64. package/dist/bundle/rpc-entry.js +1 -1
  65. package/dist/bundle/runtime-manifest.json +1 -1
  66. package/dist/core/agent-session.d.ts +12 -2
  67. package/dist/core/agent-session.js +59 -8
  68. package/dist/core/assistant-error-redaction.d.ts +9 -0
  69. package/dist/core/assistant-error-redaction.js +27 -0
  70. package/dist/core/config-reference-redaction.js +90 -10
  71. package/dist/core/context-files.d.ts +10 -0
  72. package/dist/core/context-files.js +58 -12
  73. package/dist/core/egress/wire.d.ts +13 -0
  74. package/dist/core/extensions/loader.js +24 -4
  75. package/dist/core/extensions/runner.d.ts +9 -1
  76. package/dist/core/extensions/runner.js +9 -4
  77. package/dist/core/extensions/types.d.ts +17 -3
  78. package/dist/core/hand/hook-advertise.d.ts +41 -0
  79. package/dist/core/hand/hook-advertise.js +98 -0
  80. package/dist/core/hand/hook-contract.d.ts +66 -0
  81. package/dist/core/hand/hook-contract.js +138 -0
  82. package/dist/core/resource-loader.js +2 -0
  83. package/dist/core/retry-fallback/log.d.ts +7 -1
  84. package/dist/core/retry-fallback/log.js +82 -5
  85. package/dist/core/retry-fallback/model-config-error.js +2 -1
  86. package/dist/core/settings-manager.d.ts +6 -0
  87. package/dist/core/settings-manager.js +8 -0
  88. package/dist/core/turn-origin.d.ts +19 -13
  89. package/dist/core/turn-origin.js +34 -30
  90. package/dist/core/ultrafast-lanes.d.ts +1 -0
  91. package/dist/core/ultrafast-lanes.js +4 -2
  92. package/dist/modes/interactive/components/footer.d.ts +23 -5
  93. package/dist/modes/interactive/components/footer.js +45 -16
  94. package/dist/modes/interactive/interactive-mode.js +9 -1
  95. package/dist/modes/rpc/admit-external-message-command.d.ts +2 -2
  96. package/dist/modes/rpc/admit-external-message-command.js +2 -10
  97. package/dist/modes/rpc/connection-handler.js +10 -6
  98. package/dist/modes/rpc/connection-roles.d.ts +2 -0
  99. package/dist/modes/rpc/connection-roles.js +4 -0
  100. package/dist/modes/rpc/custom-capability.d.ts +2 -0
  101. package/dist/modes/rpc/custom-capability.js +2 -0
  102. package/dist/modes/rpc/delegated-drive-origin.d.ts +6 -6
  103. package/dist/modes/rpc/delegated-drive-origin.js +7 -8
  104. package/dist/modes/rpc/delegation-declaration.d.ts +2 -0
  105. package/dist/modes/rpc/delegation-declaration.js +5 -1
  106. package/dist/modes/rpc/host-capabilities.js +3 -1
  107. package/dist/modes/rpc/loopless-tool-handlers.d.ts +6 -0
  108. package/dist/modes/rpc/loopless-tool-handlers.js +14 -1
  109. package/docs/extensions.md +10 -0
  110. package/docs/settings.md +1 -0
  111. package/docs/usage.md +1 -1
  112. package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
  113. package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
  114. package/package.json +6 -6
  115. package/dist/bundle/chunks/chunk-DLAY7CIU.js +0 -2
  116. package/dist/bundle/chunks/chunk-XH2MWTXA.js +0 -11
  117. package/dist/bundle/chunks/openai-codex-responses-ABB2J43U.js +0 -10
@@ -0,0 +1,138 @@
1
+ import { CLAIM_VERDICT_ERRORS } from "../egress/placement-claim.js";
2
+ /** The input or output cannot cross the wire: it is not JSON, or not the closed envelope. */
3
+ export class HookContractError extends Error {
4
+ constructor() {
5
+ super(...arguments);
6
+ this.name = "HookContractError";
7
+ }
8
+ }
9
+ /** The request's placement claim did not verify, so no hook ran; `code`/`retryable` are the claim's wire answer. */
10
+ export class HookRequestRefusedError extends Error {
11
+ constructor(verdict) {
12
+ super(`hook.model_request refused: placement claim ${verdict}`);
13
+ this.name = "HookRequestRefusedError";
14
+ this.verdict = verdict;
15
+ this.code = CLAIM_VERDICT_ERRORS[verdict].code;
16
+ this.retryable = CLAIM_VERDICT_ERRORS[verdict].retryable;
17
+ }
18
+ }
19
+ const isPlainObject = (value) => {
20
+ if (typeof value !== "object" || value === null || Array.isArray(value))
21
+ return false;
22
+ const prototype = Object.getPrototypeOf(value);
23
+ return prototype === Object.prototype || prototype === null;
24
+ };
25
+ /** `value` as JSON, exactly; anything JSON would drop, coerce or reject is refused (an `undefined` member is absent). */
26
+ function wireJson(value, at) {
27
+ if (value === null || typeof value === "string" || typeof value === "boolean")
28
+ return value;
29
+ if (typeof value === "number") {
30
+ if (Number.isFinite(value))
31
+ return value;
32
+ throw new HookContractError(`${at} is not a finite number`);
33
+ }
34
+ if (Array.isArray(value))
35
+ return value.map((item, index) => wireJson(item, `${at}[${index}]`));
36
+ if (isPlainObject(value)) {
37
+ const out = {};
38
+ for (const [key, member] of Object.entries(value))
39
+ if (member !== undefined)
40
+ out[key] = wireJson(member, `${at}.${key}`);
41
+ return out;
42
+ }
43
+ throw new HookContractError(`${at} is not JSON`);
44
+ }
45
+ function wireModel(model) {
46
+ const { headers: _headers, ...rest } = model;
47
+ return wireJson(rest, "model");
48
+ }
49
+ /** The projection of a fused `before_provider_request` input. */
50
+ export function hookRequestInputOf(event) {
51
+ const payload = wireJson(event.payload, "payload");
52
+ return event.model === undefined ? { payload } : { payload, model: wireModel(event.model) };
53
+ }
54
+ /** A step call's hook input; an aux call runs no request hooks on the hand and has none. */
55
+ export function hookRequestInputForCall(call, event) {
56
+ return call.kind === "step" ? hookRequestInputOf(event) : undefined;
57
+ }
58
+ /** The reply's `output`: the payload the hooks produced, with the model the request carried. */
59
+ export function hookRequestOutputOf(input, payload) {
60
+ const out = wireJson(payload, "payload");
61
+ return input.model === undefined ? { payload: out } : { payload: out, model: input.model };
62
+ }
63
+ const MODEL_STRING_FIELDS = ["id", "name", "api", "provider", "baseUrl"];
64
+ /** Reads a wire value back as the closed envelope `{ payload, model? }`; the model never carries headers. */
65
+ export function parseHookRequestInput(value) {
66
+ if (!isPlainObject(value) || !("payload" in value))
67
+ throw new HookContractError("hook input is not an object with a payload");
68
+ const extra = Object.keys(value).filter((key) => key !== "payload" && key !== "model");
69
+ if (extra.length > 0)
70
+ throw new HookContractError(`hook input has unknown members: ${extra.join(", ")}`);
71
+ const payload = wireJson(value.payload, "payload");
72
+ if (value.model === undefined)
73
+ return { payload };
74
+ const model = value.model;
75
+ if (!isPlainObject(model) || MODEL_STRING_FIELDS.some((field) => typeof model[field] !== "string"))
76
+ throw new HookContractError("hook input model is not a model");
77
+ if ("headers" in model)
78
+ throw new HookContractError("hook input model carries headers");
79
+ return { payload, model: wireJson(model, "model") };
80
+ }
81
+ /** The effective model a hand hands its hooks, as the loop sent it. */
82
+ export function hookModelOf(input) {
83
+ return input.model;
84
+ }
85
+ /** Serves one `hook.model_request`: claim first, then the closed input, then the hooks. */
86
+ export async function serveHookModelRequest(request, ports) {
87
+ const verdict = await ports.verifyClaim(request.claim);
88
+ if (verdict !== "valid")
89
+ throw new HookRequestRefusedError(verdict);
90
+ const input = parseHookRequestInput(request.input);
91
+ const produced = await ports.runRequestHooks(request.sessionId, input);
92
+ return { t: "hook.model_request.reply", sessionId: request.sessionId, stepId: request.stepId, ...produced };
93
+ }
94
+ const isHookFailure = (value) => isPlainObject(value) &&
95
+ typeof value.extensionPath === "string" &&
96
+ typeof value.event === "string" &&
97
+ typeof value.error === "string";
98
+ function hookFailures(value) {
99
+ if (!Array.isArray(value) || !value.every(isHookFailure))
100
+ throw new HookContractError("the hand's request-hook reply has malformed errors");
101
+ return value.map(({ extensionPath, event, error }) => ({ extensionPath, event, error }));
102
+ }
103
+ function wireList(value, at) {
104
+ if (!Array.isArray(value))
105
+ throw new HookContractError(`the hand's request-hook reply has malformed ${at}`);
106
+ return value.map((item, index) => wireJson(item, `${at}[${index}]`));
107
+ }
108
+ function droppedCount(value) {
109
+ if (typeof value === "number" && Number.isSafeInteger(value) && value > 0)
110
+ return value;
111
+ throw new HookContractError("the hand's request-hook reply has a malformed droppedLate");
112
+ }
113
+ /**
114
+ * `runRequestHooks` over a hand's loop-hook runner (`LooplessHand.loopHooks`): the session's hooks run on the runtime of
115
+ * `scope`, inside a loop-hook call bound to that session, with the loop busy (a model request is mid-turn). The hooks'
116
+ * failures and what they sent travel with the output, as they do on every loop-hook reply.
117
+ */
118
+ export function loopHookRequestHooks(loopHooks, scope) {
119
+ return async (sessionId, input) => {
120
+ const reply = await loopHooks("before_provider_request", {
121
+ scope: { id: scope.id, root: scope.root },
122
+ sessionId,
123
+ idle: false,
124
+ input,
125
+ });
126
+ if (!isPlainObject(reply))
127
+ throw new HookContractError("the hand's request-hook reply is not an object");
128
+ const { errors, sent, notices, droppedLate } = reply;
129
+ return {
130
+ output: parseHookRequestInput(reply.output),
131
+ ...(errors === undefined ? {} : { errors: hookFailures(errors) }),
132
+ ...(sent === undefined ? {} : { sent: wireList(sent, "sent") }),
133
+ ...(notices === undefined ? {} : { notices: wireList(notices, "notices") }),
134
+ ...(droppedLate === undefined ? {} : { droppedLate: droppedCount(droppedLate) }),
135
+ };
136
+ };
137
+ }
138
+ //# sourceMappingURL=hook-contract.js.map
@@ -569,6 +569,8 @@ export class DefaultResourceLoader {
569
569
  cwd: this.cwd,
570
570
  agentDir: this.agentDir,
571
571
  ...this.settingsManager.getSettings().contextFiles,
572
+ // senpi#5567: user settings only; a project's settings never redirect the global file.
573
+ globalPath: this.settingsManager.getGlobalContextFilePath(),
572
574
  }),
573
575
  };
574
576
  const resolvedAgentsFiles = this.agentsFilesOverride ? this.agentsFilesOverride(agentsFiles) : agentsFiles;
@@ -6,5 +6,11 @@ export interface FallbackLogger {
6
6
  export interface FallbackLoggerOptions {
7
7
  maxBytes?: number;
8
8
  }
9
- export declare function createFallbackLogger(agentDir: string, options?: FallbackLoggerOptions): FallbackLogger;
9
+ export interface SettledFallbackLogger extends FallbackLogger {
10
+ /** Resolves once the one-time removal of earlier log generations has finished and every line logged before it was written. */
11
+ settled(): Promise<void>;
12
+ }
13
+ /** Resolves once every fallback logger created so far has finished its first-start removal and flushed its queue. */
14
+ export declare function settledFallbackLoggers(): Promise<void>;
15
+ export declare function createFallbackLogger(agentDir: string, options?: FallbackLoggerOptions): SettledFallbackLogger;
10
16
  //# sourceMappingURL=log.d.ts.map
@@ -1,19 +1,81 @@
1
1
  import { closeSync, fchmodSync, mkdirSync, openSync, writeSync } from "node:fs";
2
+ import { access, mkdir, rm, writeFile } from "node:fs/promises";
2
3
  import { dirname, join } from "node:path";
3
4
  import { rotateLogIfNeeded } from "../log-file-rotation.js";
4
- import { redactSecrets } from "../sensitive-output.js";
5
+ import { redactSecrets, redactSensitiveTokenValues } from "../sensitive-output.js";
5
6
  const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
6
7
  const MAX_STRING_LENGTH = 200;
8
+ const MAX_QUEUED_LINES = 256;
7
9
  const BLOCKED_KEY = /^(?:__proto__|constructor|prototype|headers?|env(?:ironment)?|authorization|credential(?:s)?|password|secret|token|api_?key|client_?secret)$/i;
8
10
  const ALLOWED_DATA_KEY = /^(?:selector|candidate|currentSelector|originalSelector|from|to|model|provider|chainKey|reason|skipReason|classification|thinkingLevel|durationMs|retryAfterMs|error|errorMessage|warning|trigger)$/;
9
11
  const SENSITIVE_TEXT = /((?:authorization\s*[:=]\s*(?:bearer|basic)\s+)|(?:bearer\s+)|(?:[?&](?:api[_-]?key|token|secret|password|auth(?:orization)?)=))[^\s&,"'}\]]+/gi;
12
+ const unsettledLoggers = new Set();
13
+ /** Resolves once every fallback logger created so far has finished its first-start removal and flushed its queue. */
14
+ export async function settledFallbackLoggers() {
15
+ await Promise.all([...unsettledLoggers]);
16
+ }
17
+ function errorCode(failure) {
18
+ if (typeof failure === "object" && failure !== null && "code" in failure && typeof failure.code === "string") {
19
+ return failure.code;
20
+ }
21
+ return "unknown";
22
+ }
10
23
  export function createFallbackLogger(agentDir, options = {}) {
11
24
  const filePath = join(agentDir, "logs", "fallback.log");
12
25
  const maxBytes = validMaxBytes(options.maxBytes);
13
26
  let reportedWriteFailure = false;
27
+ // Lines logged while the earlier generations are still being removed wait here, so the removal can never delete them.
28
+ // They are redacted before they enter the queue and the queue is bounded: the newest lines win, the drops are counted.
29
+ let queued = [];
30
+ let dropped = 0;
31
+ function write(line) {
32
+ try {
33
+ writeLine(filePath, line, maxBytes);
34
+ }
35
+ catch (error) {
36
+ if (!reportedWriteFailure) {
37
+ reportedWriteFailure = true;
38
+ console.error("Unable to write retry fallback debug log", error);
39
+ }
40
+ }
41
+ }
42
+ const settled = retireUnsharedRegistryGenerations(filePath)
43
+ .then(() => undefined)
44
+ .catch((error) => {
45
+ reportedWriteFailure = true;
46
+ console.error("Unable to remove earlier retry fallback debug log generations", error);
47
+ return error;
48
+ })
49
+ .then((failure) => {
50
+ const lines = queued ?? [];
51
+ queued = undefined;
52
+ for (const line of lines)
53
+ write(line);
54
+ if (dropped > 0) {
55
+ write(formatLine("warn", "queued_lines_dropped", {
56
+ reason: `${dropped} lines dropped while earlier log generations were removed`,
57
+ }));
58
+ }
59
+ if (failure !== undefined) {
60
+ // The code only: the error text names the agent dir's absolute path.
61
+ write(formatLine("warn", "retire_generations_failed", {
62
+ errorMessage: `removal failed: ${errorCode(failure)}`,
63
+ }));
64
+ }
65
+ });
66
+ unsettledLoggers.add(settled);
67
+ void settled.then(() => unsettledLoggers.delete(settled));
14
68
  function log(level, event, data) {
15
69
  try {
16
70
  const line = formatLine(level, event, data);
71
+ if (queued !== undefined) {
72
+ queued.push(redactSensitiveTokenValues(line, "[redacted]"));
73
+ if (queued.length > MAX_QUEUED_LINES) {
74
+ queued.shift();
75
+ dropped += 1;
76
+ }
77
+ return;
78
+ }
17
79
  writeLine(filePath, line, maxBytes);
18
80
  }
19
81
  catch (error) {
@@ -27,8 +89,23 @@ export function createFallbackLogger(agentDir, options = {}) {
27
89
  debug: (event, data) => log("debug", event, data),
28
90
  info: (event, data) => log("info", event, data),
29
91
  warn: (event, data) => log("warn", event, data),
92
+ settled: () => settled,
30
93
  };
31
94
  }
95
+ /**
96
+ * Generations written before every engine copy in a process shared one resolved-value registry (omo#12965) are
97
+ * removed once per agent dir, the first time an engine with the shared registry starts on it; the marker makes every
98
+ * later start leave the logs alone.
99
+ */
100
+ async function retireUnsharedRegistryGenerations(filePath) {
101
+ const marker = join(dirname(filePath), ".fallback-log-v2");
102
+ if (await access(marker).then(() => true, () => false))
103
+ return;
104
+ await rm(`${filePath}.1`, { force: true });
105
+ await rm(filePath, { force: true });
106
+ await mkdir(dirname(filePath), { recursive: true, mode: 0o700 });
107
+ await writeFile(marker, "", { mode: 0o600 });
108
+ }
32
109
  function validMaxBytes(value) {
33
110
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : DEFAULT_MAX_BYTES;
34
111
  }
@@ -51,7 +128,7 @@ function formatLine(level, event, data) {
51
128
  }
52
129
  function writeLine(filePath, line, maxBytes) {
53
130
  mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
54
- const text = `${line}\n`;
131
+ const text = `${redactSensitiveTokenValues(line, "[redacted]")}\n`;
55
132
  rotateLogIfNeeded(filePath, Buffer.byteLength(text), maxBytes);
56
133
  const descriptor = openSync(filePath, "a", 0o600);
57
134
  try {
@@ -66,13 +143,13 @@ function serializeValue(value, seen) {
66
143
  if (typeof value === "string")
67
144
  return safeText(value);
68
145
  if (typeof value === "bigint")
69
- return value.toString();
146
+ return safeText(value.toString());
70
147
  if (typeof value === "number" || typeof value === "boolean" || value === null)
71
148
  return value;
72
149
  if (typeof value === "undefined")
73
150
  return undefined;
74
151
  if (typeof value === "function" || typeof value === "symbol")
75
- return String(value);
152
+ return safeText(String(value));
76
153
  if (seen.has(value))
77
154
  return "[Circular]";
78
155
  seen.add(value);
@@ -83,7 +160,7 @@ function serializeValue(value, seen) {
83
160
  if (!BLOCKED_KEY.test(key)) {
84
161
  const safeValue = serializeValue(item, seen);
85
162
  if (safeValue !== undefined)
86
- result[key] = safeValue;
163
+ result[safeText(key)] = safeValue;
87
164
  }
88
165
  }
89
166
  return result;
@@ -1,3 +1,4 @@
1
+ import { redactSecrets } from "../sensitive-output.js";
1
2
  const EXCERPT_LIMIT = 500;
2
3
  /**
3
4
  * A model configuration error is never retried or fallen back: another model would answer a request the caller did
@@ -16,7 +17,7 @@ export function createModelConfigErrorReporter(logger, emit) {
16
17
  if (reported.has(key))
17
18
  return;
18
19
  reported.add(key);
19
- const errorMessage = message.errorMessage ?? "";
20
+ const errorMessage = redactSecrets(message.errorMessage ?? "");
20
21
  logger.warn("model_config_error", { model, provider: message.provider, errorMessage });
21
22
  emit({
22
23
  type: "model_config_error",
@@ -302,6 +302,12 @@ export declare class SettingsManager {
302
302
  getSettings(): Settings;
303
303
  getGlobalSettings(): Settings;
304
304
  getProjectSettings(): Settings;
305
+ /**
306
+ * `contextFiles.globalPath` from the user's own layers only: session overrides, then global
307
+ * settings. A project's `.senpi/settings.json` never redirects the user's global context file
308
+ * (senpi#5567), so the project layer is deliberately skipped.
309
+ */
310
+ getGlobalContextFilePath(): string | undefined;
305
311
  /** The project settings file at `path` as stored: parsed and migrated like a load, but never gated by trust. */
306
312
  readProjectSettingsFile(path: string): ProjectSettingsFile;
307
313
  /**
@@ -697,6 +697,14 @@ export class SettingsManager {
697
697
  getProjectSettings() {
698
698
  return structuredClone(this.projectSettings);
699
699
  }
700
+ /**
701
+ * `contextFiles.globalPath` from the user's own layers only: session overrides, then global
702
+ * settings. A project's `.senpi/settings.json` never redirects the user's global context file
703
+ * (senpi#5567), so the project layer is deliberately skipped.
704
+ */
705
+ getGlobalContextFilePath() {
706
+ return this.runtimeOverrides.contextFiles?.globalPath ?? this.globalSettings.contextFiles?.globalPath;
707
+ }
700
708
  /** The project settings file at `path` as stored: parsed and migrated like a load, but never gated by trust. */
701
709
  readProjectSettingsFile(path) {
702
710
  let content;
@@ -1,17 +1,15 @@
1
- import type { SessionManager } from "./session-manager.ts";
2
1
  /**
3
- * Who started a turn, for the delegated-default permission rule (remote approval-mode work):
4
- * - `local`: a local connection's or the TUI's own input;
5
- * - `delegated`: input that arrived on a delegated (remote device) connection;
6
- * - `peer`: an agent's relayed delivery on a delegated connection the forwarder granted `peer_agent` (senpi#6305): it
7
- * runs under the receiving session's own preset, as the session's own work does;
2
+ * Who started a turn, for the remote approval-mode rule (senpi#6342: a turn follows the permission setting of the machine
3
+ * it runs on, and no origin outlives the turn it started):
4
+ * - `local`: this machine's own input, or input from a device the forwarder stated is the same account;
5
+ * - `delegated`: input from a device the forwarder did not state is the same account, or the one run such a device's
6
+ * record started;
8
7
  * - `engine`: a turn no client record started (an extension's message, a goal or loop turn, restored input).
9
8
  * A steer joins the running turn (`join`) and never changes its origin.
10
9
  */
11
- export type TurnOrigin = "local" | "delegated" | "peer" | "engine";
10
+ export type TurnOrigin = "local" | "delegated" | "engine";
12
11
  export type InputTurnMark = TurnOrigin | "join";
13
12
  export type ConnectionOrigin = "local" | "delegated";
14
- type OriginSessionManager = Pick<SessionManager, "getEntries" | "appendCustomEntry">;
15
13
  /** The mark an input gets where it is created: explicit for client records, `local` for the TUI, else `engine`. */
16
14
  export declare function inputTurnMark(input: {
17
15
  readonly mode?: "steer" | "followUp";
@@ -23,11 +21,19 @@ export declare function markInputTurnOrigin(message: object, mark: InputTurnMark
23
21
  export declare function copyInputTurnMark(from: object, to: object): void;
24
22
  /** Whether a message carries an origin mark: a user message always opens a turn's origin, any other only when marked. */
25
23
  export declare function hasInputTurnMark(message: object): boolean;
26
- /** A run starts as engine work; the user message that opens a turn sets its origin, a steer keeps it. */
24
+ /** Runs `handle`, and every async task it starts, as the record `origin` describes. */
25
+ export declare function runInRecordScope<T>(origin: ConnectionOrigin, handle: () => Promise<T>): Promise<T>;
26
+ /** Whether the work being created belongs to an open cross-account record's own handling. */
27
+ export declare function recordIsDelegated(): boolean;
28
+ /**
29
+ * A run starts as engine work, or as delegated work when a cross-account record's own handling starts it; a retry keeps
30
+ * the turn it continues. The user message that opens a turn sets its origin, a steer keeps it.
31
+ */
27
32
  export declare function noteRunStart(sessionManager: object): void;
33
+ /** A retry or recovery run repeats the turn it continues, so it keeps that turn's origin until the continuation ends. */
34
+ export declare function noteTurnContinuation(sessionManager: object): void;
35
+ export declare function endTurnContinuation(sessionManager: object): void;
28
36
  export declare function noteUserMessageStart(sessionManager: object, message: object): void;
29
- export declare function recordDelegatedOrigin(sessionManager: OriginSessionManager): void;
30
- /** Whether the deciding turn counts as delegated-origin: a remote device started it, or nobody did on a remote-driven session. */
31
- export declare function decidingTurnIsDelegated(sessionManager: Pick<SessionManager, "getEntries">): boolean;
32
- export {};
37
+ /** Whether the deciding turn counts as delegated-origin: a cross-account device started this turn or this run. */
38
+ export declare function decidingTurnIsDelegated(sessionManager: object): boolean;
33
39
  //# sourceMappingURL=turn-origin.d.ts.map
@@ -1,13 +1,16 @@
1
- const DELEGATED_ORIGIN_ENTRY = "delegated_origin";
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  const inputMarks = new WeakMap();
3
3
  const currentOrigins = new WeakMap();
4
- const delegatedSessions = new WeakSet();
4
+ const recordScopes = new AsyncLocalStorage();
5
+ const continuingTurns = new WeakSet();
5
6
  /** The mark an input gets where it is created: explicit for client records, `local` for the TUI, else `engine`. */
6
7
  export function inputTurnMark(input) {
7
8
  if (input.mode === "steer")
8
9
  return "join";
9
10
  if (input.connectionOrigin !== undefined)
10
11
  return input.connectionOrigin;
12
+ if (recordIsDelegated())
13
+ return "delegated";
11
14
  return input.source === undefined || input.source === "interactive" ? "local" : "engine";
12
15
  }
13
16
  export function markInputTurnOrigin(message, mark) {
@@ -23,9 +26,34 @@ export function copyInputTurnMark(from, to) {
23
26
  export function hasInputTurnMark(message) {
24
27
  return inputMarks.has(message);
25
28
  }
26
- /** A run starts as engine work; the user message that opens a turn sets its origin, a steer keeps it. */
29
+ /** Runs `handle`, and every async task it starts, as the record `origin` describes. */
30
+ export function runInRecordScope(origin, handle) {
31
+ return recordScopes.run({ origin, open: true }, handle);
32
+ }
33
+ /** Whether the work being created belongs to an open cross-account record's own handling. */
34
+ export function recordIsDelegated() {
35
+ const scope = recordScopes.getStore();
36
+ return scope?.open === true && scope.origin === "delegated";
37
+ }
38
+ /**
39
+ * A run starts as engine work, or as delegated work when a cross-account record's own handling starts it; a retry keeps
40
+ * the turn it continues. The user message that opens a turn sets its origin, a steer keeps it.
41
+ */
27
42
  export function noteRunStart(sessionManager) {
28
- currentOrigins.set(sessionManager, "engine");
43
+ if (continuingTurns.has(sessionManager))
44
+ return;
45
+ const delegated = recordIsDelegated();
46
+ const scope = recordScopes.getStore();
47
+ if (scope !== undefined)
48
+ scope.open = false;
49
+ currentOrigins.set(sessionManager, delegated ? "delegated" : "engine");
50
+ }
51
+ /** A retry or recovery run repeats the turn it continues, so it keeps that turn's origin until the continuation ends. */
52
+ export function noteTurnContinuation(sessionManager) {
53
+ continuingTurns.add(sessionManager);
54
+ }
55
+ export function endTurnContinuation(sessionManager) {
56
+ continuingTurns.delete(sessionManager);
29
57
  }
30
58
  export function noteUserMessageStart(sessionManager, message) {
31
59
  const mark = inputMarks.get(message);
@@ -33,32 +61,8 @@ export function noteUserMessageStart(sessionManager, message) {
33
61
  return;
34
62
  currentOrigins.set(sessionManager, mark ?? "engine");
35
63
  }
36
- export function recordDelegatedOrigin(sessionManager) {
37
- if (delegatedOriginOf(sessionManager))
38
- return;
39
- delegatedSessions.add(sessionManager);
40
- sessionManager.appendCustomEntry(DELEGATED_ORIGIN_ENTRY, { delegatedOrigin: true }, { startupBookkeeping: true });
41
- }
42
- // How many entries a session's scan has already found free of the marker: each gated decision reads only new entries.
43
- const scannedEntries = new WeakMap();
44
- function delegatedOriginOf(sessionManager) {
45
- if (delegatedSessions.has(sessionManager))
46
- return true;
47
- const entries = sessionManager.getEntries();
48
- const persisted = entries
49
- .slice(scannedEntries.get(sessionManager) ?? 0)
50
- .some((entry) => entry.type === "custom" && entry.customType === DELEGATED_ORIGIN_ENTRY);
51
- if (persisted)
52
- delegatedSessions.add(sessionManager);
53
- else
54
- scannedEntries.set(sessionManager, entries.length);
55
- return persisted;
56
- }
57
- /** Whether the deciding turn counts as delegated-origin: a remote device started it, or nobody did on a remote-driven session. */
64
+ /** Whether the deciding turn counts as delegated-origin: a cross-account device started this turn or this run. */
58
65
  export function decidingTurnIsDelegated(sessionManager) {
59
- const origin = currentOrigins.get(sessionManager) ?? "engine";
60
- if (origin === "delegated")
61
- return true;
62
- return origin === "engine" && delegatedOriginOf(sessionManager);
66
+ return currentOrigins.get(sessionManager) === "delegated";
63
67
  }
64
68
  //# sourceMappingURL=turn-origin.js.map
@@ -4,5 +4,6 @@ export declare function serviceTierForProvider(provider: string | undefined, ser
4
4
  export declare function ultrafastSelectionWarning(model: {
5
5
  readonly provider: string;
6
6
  readonly id: string;
7
+ readonly upstreamModelId?: string;
7
8
  }, serviceTier: ServiceTier | undefined): string | undefined;
8
9
  //# sourceMappingURL=ultrafast-lanes.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { ULTRAFAST_DOCUMENTED_MODEL_IDS } from "@earendil-works/pi-ai";
1
2
  /**
2
3
  * Providers that receive `service_tier: "ultrafast"`: OpenAI's own API and the ChatGPT
3
4
  * subscription backend. Gateways and other providers serving an OpenAI model on the Responses
@@ -5,7 +6,6 @@
5
6
  * sends Ultrafast only to first-party OpenAI and Codex models).
6
7
  */
7
8
  const ULTRAFAST_PROVIDERS = new Set(["openai", "chatgpt-subscription"]);
8
- const ULTRAFAST_DOCUMENTED_MODEL_IDS = new Set(["gpt-6-astra", "gpt-6.1-sol"]);
9
9
  export function serviceTierForProvider(provider, serviceTier) {
10
10
  if (serviceTier !== "ultrafast")
11
11
  return serviceTier;
@@ -18,7 +18,9 @@ export function ultrafastSelectionWarning(model, serviceTier) {
18
18
  if (!ULTRAFAST_PROVIDERS.has(model.provider)) {
19
19
  return `Ultrafast is only sent to OpenAI and ChatGPT Subscription; ${model.provider}/${model.id} runs at its default tier`;
20
20
  }
21
- if (ULTRAFAST_DOCUMENTED_MODEL_IDS.has(model.id))
21
+ // A tier-pinned row (for example `gpt-6.1-sol-ultrafast`) is documented by its upstream model,
22
+ // as the pricing code already checks (senpi#2978).
23
+ if (ULTRAFAST_DOCUMENTED_MODEL_IDS.has(model.upstreamModelId ?? model.id))
22
24
  return undefined;
23
25
  return `Ultrafast is documented for GPT-6 Astra and GPT-6.1 Sol; ${model.provider}/${model.id} may reject or ignore it`;
24
26
  }
@@ -10,11 +10,26 @@ export declare function formatTokens(count: number): string;
10
10
  /**
11
11
  * Mirrors the provider-shown-only-when->1 rule for accounts: the active account
12
12
  * name appears only when the provider actually pools more than one slot. The
13
- * pick shown is the pin when present, else the first account of the user order
14
- * (senpi#3591), else the session's HRW winner - the same ranking the rotation
15
- * engine uses, so the footer names the slot that will serve.
13
+ * pick shown is the effective pin (CLI > settings > stored, senpi#5983) when
14
+ * present, else the first account of the user order (senpi#3591), else the
15
+ * session's HRW winner - the same ranking the rotation engine uses, so the
16
+ * footer names the slot that will serve. The effective pin only reaches the
17
+ * footer for the Anthropic subscription provider (senpi#6222 review); other
18
+ * pooled providers keep their stored pin even when a slot shares the pin's
19
+ * name. A blocked pin (blockedUntil in the
20
+ * future) is skipped for the first usable slot in that ranking, and the
21
+ * suffix marks the blocked pin (`@B (A blocked until 14:00)`); when every
22
+ * slot is blocked it names the top-ranked slot and marks it blocked instead
23
+ * of silently naming a slot that cannot serve. Block state rides on the
24
+ * stored slots (SlotBlockState); slots without it are treated as usable.
16
25
  */
17
- export declare function accountFooterSuffix(credential: Credential | undefined, sessionId: string): string;
26
+ export type AccountFooterOptions = {
27
+ /** Effective pin from CLI/settings, outranking the stored pin (senpi#5983). */
28
+ pinnedAccount?: string;
29
+ /** Override for "now" (tests); defaults to `Date.now()`. */
30
+ now?: number;
31
+ };
32
+ export declare function accountFooterSuffix(credential: Credential | undefined, sessionId: string, options?: AccountFooterOptions): string;
18
33
  export declare function formatCwdForFooter(cwd: string, home: string | undefined): string;
19
34
  /**
20
35
  * Footer component that shows pwd, token stats, and context usage.
@@ -25,7 +40,10 @@ export declare class FooterComponent implements Component {
25
40
  private footerData;
26
41
  private autoCompactEnabled;
27
42
  private compactionDelegated;
28
- constructor(session: AgentSession, footerData: ReadonlyFooterDataProvider);
43
+ private resolveAccountPin;
44
+ constructor(session: AgentSession, footerData: ReadonlyFooterDataProvider, options?: {
45
+ resolveAccountPin?: (sessionId: string) => string | undefined;
46
+ });
29
47
  setSession(session: AgentSession): void;
30
48
  setAutoCompactEnabled(enabled: boolean): void;
31
49
  /**