@indigoai-us/hq-cli 5.345.156 → 5.345.159
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.
- package/CHANGELOG.md +231 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +531 -6
- package/dist/command-catalog.generated.js +674 -27
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +150 -10
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +12 -0
- package/dist/commands/crm.js +83 -6
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +52 -1
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +21 -0
- package/dist/commands/secrets.js +114 -14
- package/dist/commands/signals.js +17 -5
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/index-render/company-knowledge.js +1 -3
- package/dist/lib/index-render/projects.js +1 -3
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.
|
|
1
|
+
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.97","bundleSha256":"7cb110257acf96c8d91c57f238f7f01be43620ed66ae28dfe74dcc972be64153","formatVersion":1}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* item until the bot acks (`POST /v1/agents/{uid}/inbox/{id}/ack`), and the
|
|
16
16
|
* poller dedupes by id so the same item is never mirrored twice.
|
|
17
17
|
*/
|
|
18
|
+
import { createWakeSecretResolver } from "../wake-secrets.js";
|
|
18
19
|
import * as fs from "node:fs";
|
|
19
20
|
import * as path from "node:path";
|
|
20
21
|
import { vaultApiFetch } from "../../../utils/vault-api.js";
|
|
@@ -247,7 +248,7 @@ async function maybeWake(deps, freshCount) {
|
|
|
247
248
|
return;
|
|
248
249
|
const notice = buildWakeNotice(deps.agentUid, reason, pending.map((e) => e.id), now);
|
|
249
250
|
try {
|
|
250
|
-
await (deps.fireWake ?? fireWake)(config, notice, { paths: deps.paths, agentUid: deps.agentUid, log: deps.log, now: deps.now });
|
|
251
|
+
await (deps.fireWake ?? fireWake)(config, notice, { paths: deps.paths, agentUid: deps.agentUid, log: deps.log, now: deps.now, resolveHeaderSecrets: createWakeSecretResolver(deps.companySlug, deps.getToken, deps.apiBaseUrl) });
|
|
251
252
|
}
|
|
252
253
|
catch (err) {
|
|
253
254
|
deps.log("warn", `wake failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Wake-only vault reads: use the kit machine token, never cache plaintext. */
|
|
2
|
+
import { getCompanyUid, vaultApiFetch } from "../../utils/vault-api.js";
|
|
3
|
+
export declare function createWakeSecretResolver(companySlug: string | undefined, getToken: () => Promise<string>, baseUrl: string, deps?: {
|
|
4
|
+
getCompanyUid: typeof getCompanyUid;
|
|
5
|
+
vaultApiFetch: typeof vaultApiFetch;
|
|
6
|
+
}): (names: string[]) => Promise<Map<string, string>>;
|
|
7
|
+
//# sourceMappingURL=wake-secrets.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Wake-only vault reads: use the kit machine token, never cache plaintext. */
|
|
2
|
+
import { getCompanyUid, vaultApiFetch } from "../../utils/vault-api.js";
|
|
3
|
+
export function createWakeSecretResolver(companySlug, getToken, baseUrl, deps = { getCompanyUid, vaultApiFetch }) {
|
|
4
|
+
return async (names) => {
|
|
5
|
+
if (!companySlug)
|
|
6
|
+
throw new Error("Wake secrets require an enrolled company");
|
|
7
|
+
const token = await getToken();
|
|
8
|
+
const companyUid = await deps.getCompanyUid(token, companySlug, baseUrl);
|
|
9
|
+
const response = await deps.vaultApiFetch({
|
|
10
|
+
token, baseUrl, path: `/secrets/${encodeURIComponent(companyUid)}/load`, method: "POST",
|
|
11
|
+
body: { names, usage: { channel: "exec", reason: "agent kit webhook wake" } },
|
|
12
|
+
signal: AbortSignal.timeout(15_000),
|
|
13
|
+
});
|
|
14
|
+
if (!response.ok)
|
|
15
|
+
throw new Error("Wake secret authorization failed");
|
|
16
|
+
const data = await response.json();
|
|
17
|
+
if (!Array.isArray(data.secrets) || !Array.isArray(data.errors) || data.errors.length) {
|
|
18
|
+
throw new Error("Wake secret lookup failed");
|
|
19
|
+
}
|
|
20
|
+
const values = new Map();
|
|
21
|
+
for (const row of data.secrets) {
|
|
22
|
+
if (!row || typeof row.name !== "string" || !names.includes(row.name) || values.has(row.name) ||
|
|
23
|
+
typeof row.value !== "string" || row.revokedAt != null)
|
|
24
|
+
throw new Error("Invalid wake secret response");
|
|
25
|
+
values.set(row.name, row.value);
|
|
26
|
+
}
|
|
27
|
+
if (values.size !== new Set(names).size)
|
|
28
|
+
throw new Error("Wake secret missing");
|
|
29
|
+
return values;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=wake-secrets.js.map
|
|
@@ -30,12 +30,15 @@ export type WakeConfig = {
|
|
|
30
30
|
kind: "url";
|
|
31
31
|
url: string;
|
|
32
32
|
retryMs: number;
|
|
33
|
+
headerSecrets?: Record<string, string>;
|
|
33
34
|
} | {
|
|
34
35
|
kind: "command";
|
|
35
36
|
command: string[];
|
|
36
37
|
timeoutMs: number;
|
|
37
38
|
retryMs: number;
|
|
38
39
|
};
|
|
40
|
+
/** Store references only. Reject ambiguous and transport-controlled headers. */
|
|
41
|
+
export declare function parseHeaderSecrets(specs: string[]): Record<string, string>;
|
|
39
42
|
export interface WakeNotice {
|
|
40
43
|
event: "hq.inbox.pending";
|
|
41
44
|
agentUid: string;
|
|
@@ -59,6 +62,7 @@ export interface WakeDeps {
|
|
|
59
62
|
agentUid: string;
|
|
60
63
|
log: KitLogger;
|
|
61
64
|
now?: () => Date;
|
|
65
|
+
resolveHeaderSecrets?: (names: string[]) => Promise<Map<string, string>>;
|
|
62
66
|
fetchImpl?: typeof fetch;
|
|
63
67
|
spawnImpl?: typeof spawn;
|
|
64
68
|
pidAlive?: (pid: number | undefined) => boolean;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* The inbox poller fires the wake when new items are mirrored, and re-fires
|
|
19
19
|
* every `retryMs` while items stay pending, so a missed webhook heals.
|
|
20
20
|
*/
|
|
21
|
+
import { SECRET_NAME_PATTERN } from "../../commands/_patterns.js";
|
|
21
22
|
import { spawn } from "node:child_process";
|
|
22
23
|
import * as fs from "node:fs";
|
|
23
24
|
import * as path from "node:path";
|
|
@@ -27,6 +28,33 @@ export const WAKE_STATE_NAME = "wake-state.json";
|
|
|
27
28
|
export const DEFAULT_WAKE_RETRY_MS = 10 * 60_000;
|
|
28
29
|
export const WAKE_URL_TIMEOUT_MS = 15_000;
|
|
29
30
|
export const DEFAULT_WAKE_COMMAND_TIMEOUT_MS = 10 * 60_000;
|
|
31
|
+
/** Store references only. Reject ambiguous and transport-controlled headers. */
|
|
32
|
+
export function parseHeaderSecrets(specs) {
|
|
33
|
+
const result = {};
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const spec of specs) {
|
|
36
|
+
const at = spec.indexOf("=");
|
|
37
|
+
const header = spec.slice(0, at);
|
|
38
|
+
const name = spec.slice(at + 1);
|
|
39
|
+
const lower = header.toLowerCase();
|
|
40
|
+
if (at < 1 || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(header) || !SECRET_NAME_PATTERN.test(name) ||
|
|
41
|
+
["host", "content-length", "transfer-encoding", "connection", "content-type", "user-agent", "__proto__"].includes(lower) || seen.has(lower)) {
|
|
42
|
+
throw new Error("Invalid --header-secret: use a unique Header-Name=VAULT_SECRET_NAME reference");
|
|
43
|
+
}
|
|
44
|
+
seen.add(lower);
|
|
45
|
+
result[header] = name;
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
function validateHeaderSecrets(raw) {
|
|
50
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
51
|
+
throw new Error("Invalid header secret references");
|
|
52
|
+
return parseHeaderSecrets(Object.entries(raw).map(([header, name]) => {
|
|
53
|
+
if (typeof name !== "string")
|
|
54
|
+
throw new Error("Invalid header secret reference");
|
|
55
|
+
return `${header}=${name}`;
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
30
58
|
export const WAKE_INSTRUCTIONS = "New HQ messages are waiting. Run `hq agent inbox` (or the hq_inbox_read tool). Reply where each message " +
|
|
31
59
|
"arrived: if it has a channelId, post in that channel with `hq dm <channelId> \"…\"`; otherwise reply to the " +
|
|
32
60
|
"sender with `hq dm <sender-uid> \"…\"` (or hq_dm_send). Do not reply to another bot's greeting, thanks or " +
|
|
@@ -82,7 +110,7 @@ export function readWakeConfig(paths) {
|
|
|
82
110
|
const retryMs = typeof raw.retryMs === "number" && raw.retryMs >= 60_000 ? raw.retryMs : DEFAULT_WAKE_RETRY_MS;
|
|
83
111
|
if (raw.kind === "url" && typeof raw.url === "string") {
|
|
84
112
|
try {
|
|
85
|
-
return { kind: "url", url: validateWakeUrl(raw.url), retryMs };
|
|
113
|
+
return { kind: "url", url: validateWakeUrl(raw.url), retryMs, ...(raw.headerSecrets === undefined ? {} : { headerSecrets: validateHeaderSecrets(raw.headerSecrets) }) };
|
|
86
114
|
}
|
|
87
115
|
catch {
|
|
88
116
|
return null;
|
|
@@ -167,9 +195,27 @@ export async function fireWake(config, notice, deps) {
|
|
|
167
195
|
const timer = setTimeout(() => controller.abort(), WAKE_URL_TIMEOUT_MS);
|
|
168
196
|
const target = redactWakeUrl(config.url);
|
|
169
197
|
try {
|
|
198
|
+
const headers = new Headers({ "content-type": "application/json", "user-agent": "hq-agent-kit" });
|
|
199
|
+
if (config.headerSecrets !== undefined) {
|
|
200
|
+
const refs = validateHeaderSecrets(config.headerSecrets);
|
|
201
|
+
const names = [...new Set(Object.values(refs))];
|
|
202
|
+
if (names.length) {
|
|
203
|
+
if (!deps.resolveHeaderSecrets)
|
|
204
|
+
throw new Error("Secret resolution unavailable");
|
|
205
|
+
const values = await deps.resolveHeaderSecrets(names);
|
|
206
|
+
for (const [header, name] of Object.entries(refs)) {
|
|
207
|
+
const value = values.get(name);
|
|
208
|
+
if (!value || /[\r\n\0]/.test(value))
|
|
209
|
+
throw new Error("Invalid secret header value");
|
|
210
|
+
headers.set(header, value);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
170
214
|
const res = await doFetch(config.url, {
|
|
171
215
|
method: "POST",
|
|
172
|
-
headers
|
|
216
|
+
headers,
|
|
217
|
+
// Custom authentication headers must never follow a redirect.
|
|
218
|
+
redirect: "error",
|
|
173
219
|
body: JSON.stringify(notice),
|
|
174
220
|
signal: controller.signal,
|
|
175
221
|
});
|
|
@@ -180,7 +226,7 @@ export async function fireWake(config, notice, deps) {
|
|
|
180
226
|
return { fired: true, ok, detail };
|
|
181
227
|
}
|
|
182
228
|
catch (err) {
|
|
183
|
-
const detail = `wake url ${target} failed: ${err instanceof Error ? err.name === "AbortError" ? "timeout" : err.message :
|
|
229
|
+
const detail = `wake url ${target} failed: ${err instanceof Error ? err.name === "AbortError" ? "timeout" : ["ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "ECONNRESET"].includes(err.message) ? err.message : "request or secret resolution failed" : "request or secret resolution failed"}`;
|
|
184
230
|
deps.log("warn", detail);
|
|
185
231
|
return { fired: true, ok: false, detail };
|
|
186
232
|
}
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export declare const BOT_INFLIGHT_NAME = "inflight.json";
|
|
17
17
|
export interface InflightTurn {
|
|
18
|
+
/** Legacy delivery may be settled but must not enter a newly scoped model history. */
|
|
19
|
+
omitFromHistory?: boolean;
|
|
18
20
|
v: 1;
|
|
19
21
|
/** Inbox message id, or `kickoff` for the first-start kickoff turn. */
|
|
20
22
|
messageId: string;
|
|
@@ -111,9 +113,13 @@ export interface InflightTurn {
|
|
|
111
113
|
}
|
|
112
114
|
export declare function botInflightPath(dir: string): string;
|
|
113
115
|
/** Every turn that was in progress, oldest first. */
|
|
114
|
-
export declare function readInflightTurns(dir: string
|
|
116
|
+
export declare function readInflightTurns(dir: string, options?: {
|
|
117
|
+
strictIo?: boolean;
|
|
118
|
+
}): InflightTurn[];
|
|
115
119
|
/** The marker for one message, or null. */
|
|
116
120
|
export declare function readInflight(dir: string, messageId?: string): InflightTurn | null;
|
|
121
|
+
/** Preserve legacy delivery state without replaying its text into the new scope. */
|
|
122
|
+
export declare function omitInflightRecoveryHistory(dir: string, turns: InflightTurn[]): void;
|
|
117
123
|
/** Add or replace the marker for `turn.messageId`; other turns' markers stay. */
|
|
118
124
|
export declare function writeInflight(dir: string, turn: InflightTurn): void;
|
|
119
125
|
export declare function patchInflight(dir: string, messageId: string, patch: Partial<InflightTurn>): void;
|
package/dist/lib/bot/inflight.js
CHANGED
|
@@ -24,7 +24,7 @@ function isTurn(raw) {
|
|
|
24
24
|
return t?.v === 1 && typeof t.messageId === "string" && Boolean(t.target);
|
|
25
25
|
}
|
|
26
26
|
/** Every turn that was in progress, oldest first. */
|
|
27
|
-
export function readInflightTurns(dir) {
|
|
27
|
+
export function readInflightTurns(dir, options = {}) {
|
|
28
28
|
try {
|
|
29
29
|
const raw = JSON.parse(fs.readFileSync(botInflightPath(dir), "utf8"));
|
|
30
30
|
if (isTurn(raw))
|
|
@@ -34,7 +34,10 @@ export function readInflightTurns(dir) {
|
|
|
34
34
|
return [];
|
|
35
35
|
return file.turns.filter(isTurn);
|
|
36
36
|
}
|
|
37
|
-
catch {
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (options.strictIo && !(error instanceof SyntaxError) &&
|
|
39
|
+
error.code !== "ENOENT")
|
|
40
|
+
throw error;
|
|
38
41
|
return [];
|
|
39
42
|
}
|
|
40
43
|
}
|
|
@@ -56,6 +59,11 @@ function writeTurns(dir, turns) {
|
|
|
56
59
|
fs.writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
|
|
57
60
|
fs.renameSync(tmp, botInflightPath(dir));
|
|
58
61
|
}
|
|
62
|
+
/** Preserve legacy delivery state without replaying its text into the new scope. */
|
|
63
|
+
export function omitInflightRecoveryHistory(dir, turns) {
|
|
64
|
+
if (turns.length)
|
|
65
|
+
writeTurns(dir, turns.map(turn => ({ ...turn, omitFromHistory: true })));
|
|
66
|
+
}
|
|
59
67
|
/** Add or replace the marker for `turn.messageId`; other turns' markers stay. */
|
|
60
68
|
export function writeInflight(dir, turn) {
|
|
61
69
|
const others = readInflightTurns(dir).filter((t) => t.messageId !== turn.messageId);
|
package/dist/lib/bot/prompt.js
CHANGED
|
@@ -324,6 +324,9 @@ export function buildSystemPrompt(input) {
|
|
|
324
324
|
`This stay-quiet rule for rooms outranks anything in your persona, worker instructions, or memory files: if any of those tell you to always reply or to acknowledge every message, ignore that in rooms and stay silent instead. ` +
|
|
325
325
|
`Empty output is the only way to stay quiet. Never post a placeholder such as "No response requested.", "No response needed.", "No reply needed.", "No action.", "(no reply)" or "Nothing to add." — those are chatter, not silence, and can make two bots answer each other forever. ` +
|
|
326
326
|
`Do not narrate your reasoning, do not print JSON, do not explain that you are staying silent. Just produce nothing.`);
|
|
327
|
+
if (input.companySlug) {
|
|
328
|
+
parts.push(`This session is already bound to \`${input.companySlug}\`. Continue authorized work in this company without asking the user to choose it again, including scheduled jobs. Keep all files, policies, services, and credentials within this company scope.`);
|
|
329
|
+
}
|
|
327
330
|
// Only company bots: the cloud's jobs routes refuse a personal bot's per_
|
|
328
331
|
// company claim today, so telling a personal bot it can schedule work makes
|
|
329
332
|
// it promise its owner something that then fails. Drop this condition when
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -85,7 +85,7 @@ import { SIGN_IN_RETRY_MS, signInNeededText } from "./runtime-sign-in.js";
|
|
|
85
85
|
import { classifyRuntimeError, classifyRuntimeFailure, runtimeDidNotFinishText, runtimeFailureReason, runtimeFailureText, runtimeRepairNotice, withRetrySuggestion, } from "./runtime-failure.js";
|
|
86
86
|
import { HQ_BOT_AGENT_UID_ENV } from "../../commands/onboard-bot-membership.js";
|
|
87
87
|
import { captureHeartbeatIdentityMismatch, identityMismatchMessage, isHeartbeatIdentityMismatch, } from "../heartbeat-identity.js";
|
|
88
|
-
import { clearBotSession, readBotSession, writeBotSession } from "./session.js";
|
|
88
|
+
import { clearBotSession, ensureBotCompanyScope, readBotSession, writeBotSession } from "./session.js";
|
|
89
89
|
import { defaultBotStatus, patchBotStatus, readBotStatus } from "./status.js";
|
|
90
90
|
import { CLI_VERSION } from "../../cli-version.js";
|
|
91
91
|
import { CLI_UPDATE_RESTART_EXIT_CODE } from "./cli-update.js";
|
|
@@ -309,6 +309,8 @@ function defaultSleep(ms, signal) {
|
|
|
309
309
|
* one did not: `delivered` is how many parts the person has, so a retry starts
|
|
310
310
|
* where it stopped instead of repeating what they already read.
|
|
311
311
|
*/
|
|
312
|
+
class CompanyBindingError extends Error {
|
|
313
|
+
}
|
|
312
314
|
class PartialPostError extends Error {
|
|
313
315
|
parts;
|
|
314
316
|
delivered;
|
|
@@ -453,6 +455,11 @@ export async function runBot(deps) {
|
|
|
453
455
|
// Personal: every turn acts as the owner. Company: every turn acts as the bot.
|
|
454
456
|
const kind = effectiveBotKind(config);
|
|
455
457
|
const companies = effectiveBotCompanies(config);
|
|
458
|
+
// A single membership is unambiguous; personal and multi-company bots stay unbound.
|
|
459
|
+
const companySlug = config.companySlug ?? (kind === "company" && companies.length === 1 ? companies[0] : undefined);
|
|
460
|
+
const companyScopeKey = kind === "company" || companySlug !== undefined
|
|
461
|
+
? JSON.stringify({ kind, companies: [...companies].sort(), companySlug: companySlug ?? null })
|
|
462
|
+
: undefined;
|
|
456
463
|
const lockDeps = defaultPidLockDeps({
|
|
457
464
|
pid,
|
|
458
465
|
...(deps.isProcessAlive ? { isProcessAlive: deps.isProcessAlive } : {}),
|
|
@@ -521,7 +528,7 @@ export async function runBot(deps) {
|
|
|
521
528
|
// A restart is the owner's "try again" after signing in: start clean.
|
|
522
529
|
setStatus({ ...fallback(), pid, state: "starting", canDrainForPromotion: false, runtimeSignIn: null, cliVersion });
|
|
523
530
|
log("info", `starting ${config.name} on hq-cli ${cliVersion} (${config.runtime}, ${kind} bot${companies.length ? ` of ${companies.join(", ")}` : ""}) as ${config.agentUid} for ${config.ownerUid}`);
|
|
524
|
-
log("info", `${runtime.id} binary: ${describeRuntimeBinary(runtime, turnEnv(process.env, kind, config.agentUid,
|
|
531
|
+
log("info", `${runtime.id} binary: ${describeRuntimeBinary(runtime, turnEnv(process.env, kind, config.agentUid, companySlug), turnCwd)}`);
|
|
525
532
|
deps.cliUpdate?.startup(log);
|
|
526
533
|
try {
|
|
527
534
|
const record = await api.getAgent(config.agentUid);
|
|
@@ -541,6 +548,15 @@ export async function runBot(deps) {
|
|
|
541
548
|
await finish("failed", 0, `could not verify bot identity: ${err instanceof Error ? err.message : String(err)}`);
|
|
542
549
|
return { stop: async () => { }, done, status: () => null, idle: () => true };
|
|
543
550
|
}
|
|
551
|
+
try {
|
|
552
|
+
const scope = ensureBotCompanyScope(dir, companyScopeKey);
|
|
553
|
+
if (scope.reset)
|
|
554
|
+
log("info", "company scope changed or was unverified; archived prior conversations before starting fresh");
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
await finish("failed", 0, "could not isolate previous company conversations; settle any pending work under the previous settings and check workspace permissions before restarting");
|
|
558
|
+
return { stop: async () => { }, done, status: () => null, idle: () => true };
|
|
559
|
+
}
|
|
544
560
|
// ── System prompt ──────────────────────────────────────────────────────────
|
|
545
561
|
// A pack worker's bot gets the pack's "Pack setup" section (and the HQ setup
|
|
546
562
|
// takeover when the pack asks for it and setup is unfinished).
|
|
@@ -563,14 +579,14 @@ export async function runBot(deps) {
|
|
|
563
579
|
workerDir: config.workerDir,
|
|
564
580
|
workerSource: config.workerSource,
|
|
565
581
|
workerId: config.workerId,
|
|
566
|
-
companySlug
|
|
582
|
+
companySlug,
|
|
567
583
|
kind,
|
|
568
584
|
companies,
|
|
569
585
|
memoryDir: config.memoryDir,
|
|
570
586
|
sources: readPromptSources(config.hqRoot, config.workerDir, undefined, {
|
|
571
587
|
memoryDir: config.memoryDir,
|
|
572
588
|
workerSource: config.workerSource,
|
|
573
|
-
companySlug
|
|
589
|
+
companySlug,
|
|
574
590
|
}),
|
|
575
591
|
});
|
|
576
592
|
const systemPromptFile = path.join(dir, "system-prompt.md");
|
|
@@ -878,8 +894,18 @@ export async function runBot(deps) {
|
|
|
878
894
|
* A DM with no session (or whose resume id is gone) gets the recent thread
|
|
879
895
|
* prepended so a one-word reply still has the question it answers.
|
|
880
896
|
*/
|
|
897
|
+
const bindingNotices = new Set();
|
|
881
898
|
const executeTurn = async (prompt, sessionScope, tReceived, stream = {}, historyEventId) => {
|
|
882
|
-
const
|
|
899
|
+
const fresh = readBotConfig(dir);
|
|
900
|
+
if (!fresh)
|
|
901
|
+
throw new CompanyBindingError(`I couldn't read my company settings. Please restart ${config.name} after restoring its configuration; this work is still pending.`);
|
|
902
|
+
const live = fresh;
|
|
903
|
+
const liveCompanies = effectiveBotCompanies(live);
|
|
904
|
+
const liveCompanySlug = live.companySlug ?? (effectiveBotKind(live) === "company" && liveCompanies.length === 1 ? liveCompanies[0] : undefined);
|
|
905
|
+
if (liveCompanySlug !== companySlug || effectiveBotKind(live) !== kind ||
|
|
906
|
+
JSON.stringify([...liveCompanies].sort()) !== JSON.stringify([...companies].sort())) {
|
|
907
|
+
throw new CompanyBindingError(`My company settings changed. Please restart ${config.name} to load them before I continue; this work is still pending.`);
|
|
908
|
+
}
|
|
883
909
|
const remembered = readStatus()?.claudePermissionMode;
|
|
884
910
|
const actsPurelyForOwner = sessionScope === "dm" || sessionScope.startsWith("job:");
|
|
885
911
|
const composePrompt = async (session) => {
|
|
@@ -920,22 +946,22 @@ export async function runBot(deps) {
|
|
|
920
946
|
return next;
|
|
921
947
|
};
|
|
922
948
|
const runAttempt = async (session, turnPrompt) => {
|
|
923
|
-
// Company bind (worker-sourced bots): HQ's company hooks key on
|
|
949
|
+
// Company bind (worker-sourced and single-company bots): HQ's company hooks key on
|
|
924
950
|
// workspace/sessions/<sid>/meta.yaml, so the session id is fixed up front
|
|
925
951
|
// and the meta file written before every turn (idempotent; covers session
|
|
926
952
|
// resets). Claude gets the full hook set via its payload session id;
|
|
927
953
|
// Codex/Grok only see HQ_SESSION_ID.
|
|
928
954
|
const sid = session?.sessionId ?? randomUUID();
|
|
929
|
-
const env = turnEnv(process.env, kind, config.agentUid,
|
|
955
|
+
const env = turnEnv(process.env, kind, config.agentUid, companySlug);
|
|
930
956
|
let bind = { env };
|
|
931
|
-
if (
|
|
957
|
+
if (companySlug) {
|
|
932
958
|
try {
|
|
933
|
-
ensureBotSessionMeta(config.hqRoot, sid,
|
|
934
|
-
bind = { newSessionId: sid, env: { ...env, HQ_SESSION_ID: sid } };
|
|
959
|
+
ensureBotSessionMeta(config.hqRoot, sid, liveCompanySlug, undefined, now);
|
|
935
960
|
}
|
|
936
|
-
catch
|
|
937
|
-
|
|
961
|
+
catch {
|
|
962
|
+
throw new CompanyBindingError(`I couldn't bind this session to ${companySlug}. Please check the bot's workspace permissions and retry; this work is still pending.`);
|
|
938
963
|
}
|
|
964
|
+
bind = { newSessionId: sid, env: { ...env, HQ_SESSION_ID: sid } };
|
|
939
965
|
}
|
|
940
966
|
const tTurnStart = now().getTime();
|
|
941
967
|
// Model and thinking level are read fresh each turn, so a change from the
|
|
@@ -997,6 +1023,7 @@ export async function runBot(deps) {
|
|
|
997
1023
|
v: 1,
|
|
998
1024
|
runtime: config.runtime,
|
|
999
1025
|
sessionId: turn.sessionId,
|
|
1026
|
+
...(companyScopeKey !== undefined ? { companyScopeKey } : {}),
|
|
1000
1027
|
turns: (session?.turns ?? 0) + 1,
|
|
1001
1028
|
updatedAt: now().toISOString(),
|
|
1002
1029
|
}, sessionScope);
|
|
@@ -1143,7 +1170,18 @@ export async function runBot(deps) {
|
|
|
1143
1170
|
await poster.abandon();
|
|
1144
1171
|
const excerpt = err instanceof RuntimeError ? err.stderrExcerpt : err instanceof Error ? err.message : String(err);
|
|
1145
1172
|
const aborted = (err instanceof RuntimeError && err.aborted) || controller.signal.aborted;
|
|
1146
|
-
|
|
1173
|
+
if (err instanceof CompanyBindingError) {
|
|
1174
|
+
setStatus({ lastError: err.message });
|
|
1175
|
+
log("warn", err.message);
|
|
1176
|
+
if (!bindingNotices.has(args.sessionScope)) {
|
|
1177
|
+
try {
|
|
1178
|
+
await args.sendToPerson(err.message);
|
|
1179
|
+
bindingNotices.add(args.sessionScope);
|
|
1180
|
+
}
|
|
1181
|
+
catch { /* Keep the notice retryable if delivery failed. */ }
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const started = !(err instanceof CompanyBindingError) && (!(err instanceof RuntimeError) || err.started);
|
|
1147
1185
|
return { outcome: { ok: false, started, aborted, excerpt, error: err }, poster };
|
|
1148
1186
|
}
|
|
1149
1187
|
finally {
|
|
@@ -1180,7 +1218,7 @@ export async function runBot(deps) {
|
|
|
1180
1218
|
}
|
|
1181
1219
|
await sendToTarget(target, notice.text, rootEventId, "human");
|
|
1182
1220
|
};
|
|
1183
|
-
const postOne = async (target, body, rootEventId, audience) => {
|
|
1221
|
+
const postOne = async (target, body, rootEventId, audience, omitFromHistory = false) => {
|
|
1184
1222
|
const root = rootEventId ? { rootEventId } : {};
|
|
1185
1223
|
if (target.kind === "dm") {
|
|
1186
1224
|
const posted = await api.sendDm({
|
|
@@ -1189,7 +1227,7 @@ export async function runBot(deps) {
|
|
|
1189
1227
|
...root,
|
|
1190
1228
|
...(audience ? { audience } : {}),
|
|
1191
1229
|
});
|
|
1192
|
-
if (audience !== "agent") {
|
|
1230
|
+
if (audience !== "agent" && !omitFromHistory) {
|
|
1193
1231
|
const eventId = posted && typeof posted === "object" && "eventId" in posted && typeof posted.eventId === "string"
|
|
1194
1232
|
? posted.eventId
|
|
1195
1233
|
: undefined;
|
|
@@ -1235,10 +1273,10 @@ export async function runBot(deps) {
|
|
|
1235
1273
|
* PartialPostError naming how many parts got through, so the caller can save
|
|
1236
1274
|
* exactly the remainder.
|
|
1237
1275
|
*/
|
|
1238
|
-
const sendParts = async (target, parts, from, rootEventId, onDelivered, audience) => {
|
|
1276
|
+
const sendParts = async (target, parts, from, rootEventId, onDelivered, audience, omitFromHistory = false) => {
|
|
1239
1277
|
for (let i = Math.max(0, from); i < parts.length; i += 1) {
|
|
1240
1278
|
try {
|
|
1241
|
-
await postOne(target, parts[i], rootEventId, audience);
|
|
1279
|
+
await postOne(target, parts[i], rootEventId, audience, omitFromHistory);
|
|
1242
1280
|
}
|
|
1243
1281
|
catch (err) {
|
|
1244
1282
|
throw new PartialPostError(parts, i, err);
|
|
@@ -1459,7 +1497,7 @@ export async function runBot(deps) {
|
|
|
1459
1497
|
const parts = marker.replyParts ?? splitBody(recoveryBody(marker));
|
|
1460
1498
|
const from = marker.replyParts ? Math.min(Math.max(0, marker.replyPartsDelivered ?? 0), parts.length) : 0;
|
|
1461
1499
|
try {
|
|
1462
|
-
await sendParts(marker.target, parts, from, marker.rootEventId, (count) => patchInflight(dir, marker.messageId, { replyParts: parts, replyPartsDelivered: count }));
|
|
1500
|
+
await sendParts(marker.target, parts, from, marker.rootEventId, (count) => patchInflight(dir, marker.messageId, { replyParts: parts, replyPartsDelivered: count }), undefined, marker.omitFromHistory);
|
|
1463
1501
|
}
|
|
1464
1502
|
catch (err) {
|
|
1465
1503
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -1471,12 +1509,12 @@ export async function runBot(deps) {
|
|
|
1471
1509
|
// no part of this run reached the chat yet.
|
|
1472
1510
|
const headerOut = (marker.progressPosted ?? 0) > 0 || (err instanceof PartialPostError ? err.delivered : from) > 0;
|
|
1473
1511
|
const text = marker.jobHeader && !headerOut ? `${marker.jobHeader}${UNDELIVERABLE_JOB_REPORT_TEXT}` : UNDELIVERABLE_JOB_REPORT_TEXT;
|
|
1474
|
-
await postOne(marker.target, text, marker.rootEventId).catch(() => undefined);
|
|
1512
|
+
await postOne(marker.target, text, marker.rootEventId, undefined, marker.omitFromHistory).catch(() => undefined);
|
|
1475
1513
|
// Giving up on DELIVERY is not giving up on the RUN: report it.
|
|
1476
1514
|
await reportRecoveredJobRun(marker);
|
|
1477
1515
|
}
|
|
1478
1516
|
else {
|
|
1479
|
-
await postOne(marker.target, UNDELIVERABLE_REPLY_TEXT, marker.rootEventId).catch(() => undefined);
|
|
1517
|
+
await postOne(marker.target, UNDELIVERABLE_REPLY_TEXT, marker.rootEventId, undefined, marker.omitFromHistory).catch(() => undefined);
|
|
1480
1518
|
}
|
|
1481
1519
|
await closeMarker(marker, isKickoff);
|
|
1482
1520
|
return true;
|
|
@@ -1592,6 +1630,10 @@ export async function runBot(deps) {
|
|
|
1592
1630
|
workingNotice: false,
|
|
1593
1631
|
});
|
|
1594
1632
|
await clearThinking();
|
|
1633
|
+
if (!outcome.ok && outcome.error instanceof CompanyBindingError) {
|
|
1634
|
+
clearInflight(dir, KICKOFF_MESSAGE_ID);
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1595
1637
|
if (outcome.ok) {
|
|
1596
1638
|
// The kickoff never runs silenceable, so outcome.silent is never set here.
|
|
1597
1639
|
if (poster.postedCount === 0) {
|
|
@@ -1758,6 +1800,11 @@ export async function runBot(deps) {
|
|
|
1758
1800
|
workingNotice: false,
|
|
1759
1801
|
...(quietJob ? { suppressOutput: true } : {}),
|
|
1760
1802
|
});
|
|
1803
|
+
if (!outcome.ok && outcome.error instanceof CompanyBindingError) {
|
|
1804
|
+
clearInflight(dir, id);
|
|
1805
|
+
attempts.delete(id);
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1761
1808
|
if (outcome.ok) {
|
|
1762
1809
|
if (!quietJob && !reportDelivered(outcome.reply)) {
|
|
1763
1810
|
// The work finished but the REPORT did not fully reach the DM — even if
|
|
@@ -2076,6 +2123,11 @@ export async function runBot(deps) {
|
|
|
2076
2123
|
});
|
|
2077
2124
|
// The model saw the note once its turn ran. A turn that never started, or
|
|
2078
2125
|
// one held for a coding-tool sign-in, runs again and keeps the note for it.
|
|
2126
|
+
if (!outcome.ok && outcome.error instanceof CompanyBindingError) {
|
|
2127
|
+
clearInflight(dir, id);
|
|
2128
|
+
attempts.delete(id);
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2079
2131
|
if (setupNote && !toolOffer && (outcome.ok || (outcome.started && !isSignInFailure(outcome.excerpt)))) {
|
|
2080
2132
|
try {
|
|
2081
2133
|
patchSetupIntroState(dir, { notePending: false });
|
|
@@ -10,10 +10,24 @@ export interface BotSessionFile {
|
|
|
10
10
|
v: 1;
|
|
11
11
|
runtime: BotRuntimeId;
|
|
12
12
|
sessionId: string;
|
|
13
|
+
/** Company/membership fingerprint under which this transcript was created. */
|
|
14
|
+
companyScopeKey?: string;
|
|
13
15
|
turns: number;
|
|
14
16
|
updatedAt: string;
|
|
15
17
|
}
|
|
16
18
|
export declare function readBotSession(dir: string, runtime: BotRuntimeId, scope?: BotSessionScope): BotSessionFile | null;
|
|
17
19
|
export declare function writeBotSession(dir: string, session: BotSessionFile, scope?: BotSessionScope): void;
|
|
18
20
|
export declare function clearBotSession(dir: string, scope?: BotSessionScope): void;
|
|
21
|
+
/**
|
|
22
|
+
* Run once at startup, before reading or appending conversation state. A scope
|
|
23
|
+
* change must never resume a prior company's model session or replay its DM
|
|
24
|
+
* history. Preserve those files privately for recovery, then record the new
|
|
25
|
+
* scope. Filesystem failures propagate so callers cannot start a model turn.
|
|
26
|
+
* Legacy personal bots retain their existing history; company bots require
|
|
27
|
+
* positive scope evidence and archive legacy, unproven conversations once.
|
|
28
|
+
*/
|
|
29
|
+
export declare function ensureBotCompanyScope(dir: string, companyScopeKey: string | undefined): {
|
|
30
|
+
reset: boolean;
|
|
31
|
+
archiveDir?: string;
|
|
32
|
+
};
|
|
19
33
|
//# sourceMappingURL=session.d.ts.map
|
package/dist/lib/bot/session.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { omitInflightRecoveryHistory, readInflightTurns } from "./inflight.js";
|
|
9
11
|
import { botSessionPath } from "./paths.js";
|
|
10
12
|
export function readBotSession(dir, runtime, scope = "dm") {
|
|
11
13
|
const p = botSessionPath(dir, scope);
|
|
@@ -36,4 +38,72 @@ export function clearBotSession(dir, scope = "dm") {
|
|
|
36
38
|
/* already gone */
|
|
37
39
|
}
|
|
38
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Run once at startup, before reading or appending conversation state. A scope
|
|
43
|
+
* change must never resume a prior company's model session or replay its DM
|
|
44
|
+
* history. Preserve those files privately for recovery, then record the new
|
|
45
|
+
* scope. Filesystem failures propagate so callers cannot start a model turn.
|
|
46
|
+
* Legacy personal bots retain their existing history; company bots require
|
|
47
|
+
* positive scope evidence and archive legacy, unproven conversations once.
|
|
48
|
+
*/
|
|
49
|
+
export function ensureBotCompanyScope(dir, companyScopeKey) {
|
|
50
|
+
const marker = path.join(dir, "company-scope.json");
|
|
51
|
+
const hasMarker = fs.existsSync(marker);
|
|
52
|
+
if (!hasMarker && companyScopeKey === undefined)
|
|
53
|
+
return { reset: false };
|
|
54
|
+
const expected = companyScopeKey ?? null;
|
|
55
|
+
let provenPreviousScope = false;
|
|
56
|
+
if (hasMarker) {
|
|
57
|
+
// Read errors fail closed. Invalid JSON is unproven provenance and resets.
|
|
58
|
+
const body = fs.readFileSync(marker, "utf8");
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(body);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
parsed = null;
|
|
65
|
+
}
|
|
66
|
+
provenPreviousScope = Boolean(parsed && typeof parsed === "object" &&
|
|
67
|
+
parsed.v === 1 &&
|
|
68
|
+
(typeof parsed.companyScopeKey === "string" ||
|
|
69
|
+
parsed.companyScopeKey === null));
|
|
70
|
+
if (parsed && typeof parsed === "object" &&
|
|
71
|
+
parsed.v === 1 &&
|
|
72
|
+
parsed.companyScopeKey === expected) {
|
|
73
|
+
return { reset: false };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// A known company switch must settle recovery under its previous scope.
|
|
77
|
+
// Legacy bots have no scope evidence: isolate their model context below,
|
|
78
|
+
// but leave delivery/job markers intact for the existing recovery path.
|
|
79
|
+
// Recovery only settles saved work; it never resumes the old model session.
|
|
80
|
+
// Match recovery's tolerant parser so corrupt markers do not brick startup.
|
|
81
|
+
const pending = readInflightTurns(dir, { strictIo: true });
|
|
82
|
+
if (provenPreviousScope && pending.length > 0) {
|
|
83
|
+
throw new Error("Bot company settings changed with pending work. Restore the previous company settings and settle pending work before switching companies.");
|
|
84
|
+
}
|
|
85
|
+
if (!provenPreviousScope)
|
|
86
|
+
omitInflightRecoveryHistory(dir, pending);
|
|
87
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
88
|
+
const names = fs.readdirSync(dir).filter((name) => /^(?:session|dm-history)(?:-[A-Za-z0-9_-]+)?\.json$/.test(name));
|
|
89
|
+
let archiveDir;
|
|
90
|
+
if (names.length) {
|
|
91
|
+
const archiveRoot = path.join(dir, "company-scope-archives");
|
|
92
|
+
fs.mkdirSync(archiveRoot, { recursive: true, mode: 0o700 });
|
|
93
|
+
archiveDir = path.join(archiveRoot, randomUUID());
|
|
94
|
+
fs.mkdirSync(archiveDir, { mode: 0o700 });
|
|
95
|
+
for (const name of names)
|
|
96
|
+
fs.renameSync(path.join(dir, name), path.join(archiveDir, name));
|
|
97
|
+
}
|
|
98
|
+
// Commit the new provenance only after every old conversation was isolated.
|
|
99
|
+
const tmp = path.join(dir, `.company-scope.${randomUUID()}.tmp`);
|
|
100
|
+
try {
|
|
101
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ v: 1, companyScopeKey: expected })}\n`, { mode: 0o600 });
|
|
102
|
+
fs.renameSync(tmp, marker);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
fs.rmSync(tmp, { force: true });
|
|
106
|
+
}
|
|
107
|
+
return { reset: names.length > 0, ...(archiveDir ? { archiveDir } : {}) };
|
|
108
|
+
}
|
|
39
109
|
//# sourceMappingURL=session.js.map
|
|
@@ -287,6 +287,12 @@ export declare function tokenFingerprint(token: string): string;
|
|
|
287
287
|
export declare function readClaudeRefreshLast(root: string, home: string): ClaudeRefreshLastEntry | null;
|
|
288
288
|
/** @deprecated prefer {@link revokedMarkerForCurrentCredential}; this is the pre-fingerprint shim. */
|
|
289
289
|
export declare function isAccountRevokedPersisted(root: string, accountId: string): boolean;
|
|
290
|
+
/**
|
|
291
|
+
* True when the capacity collector has recorded this refresh token as
|
|
292
|
+
* revoked (by token fingerprint, under any account). Lanes use it to tell a
|
|
293
|
+
* dead sign-in from an access token that Claude Code will renew at startup.
|
|
294
|
+
*/
|
|
295
|
+
export declare function isRefreshTokenRevoked(root: string, refreshToken: string): boolean;
|
|
290
296
|
/**
|
|
291
297
|
* Compare the persisted revoked marker (if any) against the refresh token the
|
|
292
298
|
* caller is about to use.
|
|
@@ -1556,6 +1556,18 @@ export function isAccountRevokedPersisted(root, accountId) {
|
|
|
1556
1556
|
const file = readRefreshLast(root);
|
|
1557
1557
|
return Object.prototype.hasOwnProperty.call(file.revokedAccounts, accountId);
|
|
1558
1558
|
}
|
|
1559
|
+
/**
|
|
1560
|
+
* True when the capacity collector has recorded this refresh token as
|
|
1561
|
+
* revoked (by token fingerprint, under any account). Lanes use it to tell a
|
|
1562
|
+
* dead sign-in from an access token that Claude Code will renew at startup.
|
|
1563
|
+
*/
|
|
1564
|
+
export function isRefreshTokenRevoked(root, refreshToken) {
|
|
1565
|
+
const file = readRefreshLast(root);
|
|
1566
|
+
const fp = tokenFingerprint(refreshToken);
|
|
1567
|
+
if (file.revokedTokens[fp] !== undefined)
|
|
1568
|
+
return true;
|
|
1569
|
+
return Object.values(file.revokedAccounts).some((entry) => entry.fingerprint === fp);
|
|
1570
|
+
}
|
|
1559
1571
|
/**
|
|
1560
1572
|
* Compare the persisted revoked marker (if any) against the refresh token the
|
|
1561
1573
|
* caller is about to use.
|
|
@@ -20,6 +20,12 @@ export interface LaneLoginInput {
|
|
|
20
20
|
* login confirms sign-in through this probe. Defaults to the lanes probe.
|
|
21
21
|
*/
|
|
22
22
|
probeProvider?: Ctx["probeProvider"];
|
|
23
|
+
/**
|
|
24
|
+
* Claude access-token expiry check for the post-login status. Defaults to
|
|
25
|
+
* the real Keychain/file read when the default probe is used; a caller that
|
|
26
|
+
* injects a probe passes its own reader or gets no expiry check.
|
|
27
|
+
*/
|
|
28
|
+
readClaudeCredential?: Ctx["readClaudeCredential"];
|
|
23
29
|
}
|
|
24
30
|
export interface LaneLoginResult {
|
|
25
31
|
id: string;
|