@indigoai-us/hq-cli 5.228.3 → 5.229.0
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 +16 -0
- package/dist/commands/lanes.js +40 -1
- package/dist/lib/checkpoint-ontology/hints.d.ts +41 -0
- package/dist/lib/checkpoint-ontology/hints.js +146 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +11 -0
- package/dist/lib/checkpoint-ontology/plan.js +34 -3
- package/dist/lib/lanes/spawn.js +9 -4
- package/dist/lib/lanes/types.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.229.0] — 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Behind the off-by-default `checkpoint.ontology.entity-hints` flag, each
|
|
10
|
+
knowledge input in the checkpoint ontology plan lists up to 25 existing
|
|
11
|
+
entities whose name appears in the text the model reads
|
|
12
|
+
(`known_entities`). The model is told to use those names instead of naming a
|
|
13
|
+
new entity, and to skip glossary or translation table rows, paths and names
|
|
14
|
+
built from tags or slugs.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Account history now distinguishes automatic moves caused by a quota hold, an
|
|
19
|
+
unavailable or logged-out account, or uneven load.
|
|
20
|
+
|
|
5
21
|
## [5.228.3] — 2026-09-27
|
|
6
22
|
|
|
7
23
|
### Fixed
|
package/dist/commands/lanes.js
CHANGED
|
@@ -5635,9 +5635,20 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
|
|
|
5635
5635
|
...(model !== undefined ? { model } : {}),
|
|
5636
5636
|
unavailableHosts: balanceHosts.unavailableHosts,
|
|
5637
5637
|
});
|
|
5638
|
+
const historyReason = lane.account === undefined
|
|
5639
|
+
? "load_balance"
|
|
5640
|
+
: currentConfig?.enabled !== true
|
|
5641
|
+
? "account_unavailable"
|
|
5642
|
+
: currentLogin === false
|
|
5643
|
+
? "recorded_account_logged_out"
|
|
5644
|
+
: currentLogin !== true
|
|
5645
|
+
? "account_unavailable"
|
|
5646
|
+
: quotaBlockFor(loadQuotaState(hqRoot), "claude", lane.account, model, ctx.now()).blocked
|
|
5647
|
+
? "usage_limit"
|
|
5648
|
+
: "load_balance";
|
|
5638
5649
|
return {
|
|
5639
5650
|
selection: { ...selected, selected_at: ctx.now().toISOString() },
|
|
5640
|
-
|
|
5651
|
+
historyReason,
|
|
5641
5652
|
...(preservedModel !== undefined ? { preservedModel } : {}),
|
|
5642
5653
|
};
|
|
5643
5654
|
}
|
|
@@ -6269,6 +6280,7 @@ async function applyClaudeBalanceMove(ctx, suggestion) {
|
|
|
6269
6280
|
const moved = await prepareAccountMove(ctx, lane, {
|
|
6270
6281
|
account: suggestion.to,
|
|
6271
6282
|
cliModel: undefined,
|
|
6283
|
+
accountHistoryReason: automaticBalanceMoveHistoryReason(ctx, lane),
|
|
6272
6284
|
deferTurnStart: true,
|
|
6273
6285
|
});
|
|
6274
6286
|
return { lane_id: moved.lane.lane_id, host: moved.lane.host, from: moved.from ?? suggestion.from, to: moved.to, state: moved.lane.state };
|
|
@@ -6324,6 +6336,7 @@ async function runClaudeBalanceMoveTarget(ctx, laneId, opts) {
|
|
|
6324
6336
|
const moved = await prepareAccountMove(ctx, lane, {
|
|
6325
6337
|
account,
|
|
6326
6338
|
cliModel: undefined,
|
|
6339
|
+
accountHistoryReason: automaticBalanceMoveHistoryReason(ctx, lane),
|
|
6327
6340
|
deferTurnStart: true,
|
|
6328
6341
|
});
|
|
6329
6342
|
printJson({
|
|
@@ -6335,6 +6348,32 @@ async function runClaudeBalanceMoveTarget(ctx, laneId, opts) {
|
|
|
6335
6348
|
account: moved.lane.account,
|
|
6336
6349
|
});
|
|
6337
6350
|
}
|
|
6351
|
+
function automaticBalanceMoveHistoryReason(ctx, lane) {
|
|
6352
|
+
const hqRoot = ctx.hqRoot();
|
|
6353
|
+
const config = loadLanesConfig(hqRoot);
|
|
6354
|
+
const accountId = lane.account;
|
|
6355
|
+
if (accountId === undefined)
|
|
6356
|
+
return "load_balance";
|
|
6357
|
+
const source = config.providers[lane.provider].accounts[accountId];
|
|
6358
|
+
if (source?.enabled !== true)
|
|
6359
|
+
return "account_unavailable";
|
|
6360
|
+
const login = accountStatus(lane.provider, accountId, config, {
|
|
6361
|
+
existsSync: ctx.existsSync,
|
|
6362
|
+
hostname: ctx.hostname(),
|
|
6363
|
+
probe: ctx.probeProvider,
|
|
6364
|
+
env: ctx.env,
|
|
6365
|
+
}).logged_in;
|
|
6366
|
+
if (login === false)
|
|
6367
|
+
return "recorded_account_logged_out";
|
|
6368
|
+
if (login !== true)
|
|
6369
|
+
return "account_unavailable";
|
|
6370
|
+
const provider = config.providers[lane.provider];
|
|
6371
|
+
const model = lane.model ?? source.model ?? provider.model;
|
|
6372
|
+
if (quotaBlockFor(loadQuotaState(hqRoot), lane.provider, accountId, model, ctx.now()).blocked) {
|
|
6373
|
+
return "usage_limit";
|
|
6374
|
+
}
|
|
6375
|
+
return "load_balance";
|
|
6376
|
+
}
|
|
6338
6377
|
function laneWithRecoveredProviderSession(hqRoot, lane) {
|
|
6339
6378
|
const manifest = readSpawnManifest(laneRunDir(hqRoot, lane.lane_id));
|
|
6340
6379
|
const sessionId = (typeof lane.provider_session_id === "string" && lane.provider_session_id.trim()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Existing entities an input names, for `checkpoint.ontology.entity-hints`.
|
|
3
|
+
*
|
|
4
|
+
* The model never sees the company's entities, so it can only link one when it
|
|
5
|
+
* happens to pick a name or alias the index holds. On the CO-39 fixtures it
|
|
6
|
+
* created `SpaceXAI` beside `xAI`, `Indigo desktop app` beside `indigo-nx`
|
|
7
|
+
* (alias `Indigo desktop repo`) and `hq-work-site` beside `Indigo Workspace`
|
|
8
|
+
* (workspace/checkpoint-ontology/analysis/co41-wrong-creates.json). Each
|
|
9
|
+
* input's plan entry lists the existing entities whose name the input text
|
|
10
|
+
* contains, so the model can use that name.
|
|
11
|
+
*/
|
|
12
|
+
import type { EntityIndex } from "./tree.js";
|
|
13
|
+
export declare const ENTITY_HINTS: {
|
|
14
|
+
/** Entities listed per input. */
|
|
15
|
+
readonly perInput: 25;
|
|
16
|
+
/** An alias match counts this fraction of a canonical-name match with the same words. */
|
|
17
|
+
readonly aliasWeight: 0.5;
|
|
18
|
+
/** Aliases shown per entity. */
|
|
19
|
+
readonly aliases: 3;
|
|
20
|
+
/** Longest name, in words, matched through the n-gram set. Longer names use a phrase search. */
|
|
21
|
+
readonly maxGram: 4;
|
|
22
|
+
};
|
|
23
|
+
export interface EntityHint {
|
|
24
|
+
type: string;
|
|
25
|
+
name: string;
|
|
26
|
+
aliases: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Inverse document frequency of each word over the entities' match names, so
|
|
30
|
+
* `Sentry` outranks `HQ Build` in a document that names both.
|
|
31
|
+
*/
|
|
32
|
+
export declare function wordWeights(index: EntityIndex): Map<string, number>;
|
|
33
|
+
/**
|
|
34
|
+
* Up to `ENTITY_HINTS.perInput` writable entities whose canonical name or
|
|
35
|
+
* qualifying alias appears in `text` as whole words within one sentence, table
|
|
36
|
+
* cell or markdown block, highest weight first (ties by path). A match scores the summed word weights, halved for an alias.
|
|
37
|
+
* Entities with the same type and canonical name are listed once. Only
|
|
38
|
+
* entities from `index`, which is the plan's own company.
|
|
39
|
+
*/
|
|
40
|
+
export declare function entityHints(index: EntityIndex, weights: Map<string, number>, text: string): EntityHint[];
|
|
41
|
+
//# sourceMappingURL=hints.d.ts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Existing entities an input names, for `checkpoint.ontology.entity-hints`.
|
|
3
|
+
*
|
|
4
|
+
* The model never sees the company's entities, so it can only link one when it
|
|
5
|
+
* happens to pick a name or alias the index holds. On the CO-39 fixtures it
|
|
6
|
+
* created `SpaceXAI` beside `xAI`, `Indigo desktop app` beside `indigo-nx`
|
|
7
|
+
* (alias `Indigo desktop repo`) and `hq-work-site` beside `Indigo Workspace`
|
|
8
|
+
* (workspace/checkpoint-ontology/analysis/co41-wrong-creates.json). Each
|
|
9
|
+
* input's plan entry lists the existing entities whose name the input text
|
|
10
|
+
* contains, so the model can use that name.
|
|
11
|
+
*/
|
|
12
|
+
export const ENTITY_HINTS = {
|
|
13
|
+
/** Entities listed per input. */
|
|
14
|
+
perInput: 25,
|
|
15
|
+
/** An alias match counts this fraction of a canonical-name match with the same words. */
|
|
16
|
+
aliasWeight: 0.5,
|
|
17
|
+
/** Aliases shown per entity. */
|
|
18
|
+
aliases: 3,
|
|
19
|
+
/** Longest name, in words, matched through the n-gram set. Longer names use a phrase search. */
|
|
20
|
+
maxGram: 4,
|
|
21
|
+
};
|
|
22
|
+
const HINT_TYPES = new Set(["person", "project", "company", "concept"]);
|
|
23
|
+
/** Token that stands for a sentence, cell or block boundary. A name never matches across one it does not contain. */
|
|
24
|
+
const BOUNDARY = "\n";
|
|
25
|
+
const TOKEN = /[\p{L}\p{N}]+|[.!?;:](?=\s|$)|\|/gu;
|
|
26
|
+
/**
|
|
27
|
+
* A line that continues the previous one when the previous one is prose or a
|
|
28
|
+
* list item: hard-wrapped markdown keeps a name that wraps across the line
|
|
29
|
+
* break. Blank lines, headings, list items, table rows, quotes and fences start
|
|
30
|
+
* a new block.
|
|
31
|
+
*/
|
|
32
|
+
function continuesProse(line) {
|
|
33
|
+
return line.trim() !== "" && !/^\s*(#|[-*+]\s|\d+[.)]\s|\||>|```|~~~)/.test(line);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Lowercased words, with `BOUNDARY` between sentences (`.`, `!`, `?`, `;` or
|
|
37
|
+
* `:` before whitespace), table cells and markdown blocks. Names and text are
|
|
38
|
+
* split the same way, so a name that holds a boundary, such as `Tip 001:
|
|
39
|
+
* checkpoint`, still matches its own text.
|
|
40
|
+
*/
|
|
41
|
+
function tokens(text) {
|
|
42
|
+
const out = [];
|
|
43
|
+
const push = (token) => {
|
|
44
|
+
if (token !== BOUNDARY || (out.length > 0 && out[out.length - 1] !== BOUNDARY))
|
|
45
|
+
out.push(token);
|
|
46
|
+
};
|
|
47
|
+
let prevProse = null;
|
|
48
|
+
for (const line of text.toLowerCase().split("\n")) {
|
|
49
|
+
const prose = continuesProse(line);
|
|
50
|
+
if (prevProse !== null && !(prose && prevProse))
|
|
51
|
+
push(BOUNDARY);
|
|
52
|
+
for (const match of line.matchAll(TOKEN))
|
|
53
|
+
push(/^[\p{L}\p{N}]/u.test(match[0]) ? match[0] : BOUNDARY);
|
|
54
|
+
prevProse = prose || /^\s*([-*+]|\d+[.)])\s/.test(line);
|
|
55
|
+
}
|
|
56
|
+
while (out[0] === BOUNDARY)
|
|
57
|
+
out.shift();
|
|
58
|
+
while (out[out.length - 1] === BOUNDARY)
|
|
59
|
+
out.pop();
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function words(name) {
|
|
63
|
+
return name.filter((token) => token !== BOUNDARY);
|
|
64
|
+
}
|
|
65
|
+
function matchNames(entity) {
|
|
66
|
+
const out = [];
|
|
67
|
+
const canonical = tokens(entity.canonical_name);
|
|
68
|
+
if (words(canonical).length > 0)
|
|
69
|
+
out.push({ tokens: canonical, weight: 1 });
|
|
70
|
+
for (const alias of entity.aliases) {
|
|
71
|
+
const aliasTokens = tokens(alias);
|
|
72
|
+
const count = words(aliasTokens).length;
|
|
73
|
+
if (count >= 2 || (count === 1 && /[-_./0-9]/.test(alias))) {
|
|
74
|
+
out.push({ tokens: aliasTokens, weight: ENTITY_HINTS.aliasWeight, alias });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Inverse document frequency of each word over the entities' match names, so
|
|
81
|
+
* `Sentry` outranks `HQ Build` in a document that names both.
|
|
82
|
+
*/
|
|
83
|
+
export function wordWeights(index) {
|
|
84
|
+
const counts = new Map();
|
|
85
|
+
let total = 0;
|
|
86
|
+
for (const entity of index.byRel.values()) {
|
|
87
|
+
if (!HINT_TYPES.has(entity.type))
|
|
88
|
+
continue;
|
|
89
|
+
total += 1;
|
|
90
|
+
for (const word of new Set(matchNames(entity).flatMap((name) => words(name.tokens))))
|
|
91
|
+
counts.set(word, (counts.get(word) ?? 0) + 1);
|
|
92
|
+
}
|
|
93
|
+
const weights = new Map();
|
|
94
|
+
for (const [word, count] of counts)
|
|
95
|
+
weights.set(word, Math.log((total + 1) / count));
|
|
96
|
+
return weights;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Up to `ENTITY_HINTS.perInput` writable entities whose canonical name or
|
|
100
|
+
* qualifying alias appears in `text` as whole words within one sentence, table
|
|
101
|
+
* cell or markdown block, highest weight first (ties by path). A match scores the summed word weights, halved for an alias.
|
|
102
|
+
* Entities with the same type and canonical name are listed once. Only
|
|
103
|
+
* entities from `index`, which is the plan's own company.
|
|
104
|
+
*/
|
|
105
|
+
export function entityHints(index, weights, text) {
|
|
106
|
+
const textTokens = tokens(text);
|
|
107
|
+
const grams = new Set();
|
|
108
|
+
for (let i = 0; i < textTokens.length; i += 1) {
|
|
109
|
+
for (let n = 1; n <= ENTITY_HINTS.maxGram && i + n <= textTokens.length; n += 1) {
|
|
110
|
+
grams.add(textTokens.slice(i, i + n).join(" "));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const padded = ` ${textTokens.join(" ")} `;
|
|
114
|
+
const best = new Map();
|
|
115
|
+
for (const entity of index.byRel.values()) {
|
|
116
|
+
if (!HINT_TYPES.has(entity.type))
|
|
117
|
+
continue;
|
|
118
|
+
let score = 0;
|
|
119
|
+
const matched = [];
|
|
120
|
+
for (const name of matchNames(entity)) {
|
|
121
|
+
const phrase = name.tokens.join(" ");
|
|
122
|
+
const found = name.tokens.length <= ENTITY_HINTS.maxGram ? grams.has(phrase) : padded.includes(` ${phrase} `);
|
|
123
|
+
if (!found)
|
|
124
|
+
continue;
|
|
125
|
+
if (name.alias !== undefined)
|
|
126
|
+
matched.push(name.alias);
|
|
127
|
+
score = Math.max(score, name.weight * words(name.tokens).reduce((sum, word) => sum + (weights.get(word) ?? 0), 0));
|
|
128
|
+
}
|
|
129
|
+
if (score <= 0)
|
|
130
|
+
continue;
|
|
131
|
+
const key = `${entity.type}\0${entity.canonical_name.toLowerCase()}`;
|
|
132
|
+
const held = best.get(key);
|
|
133
|
+
if (!held || score > held.score || (score === held.score && entity.rel < held.entity.rel))
|
|
134
|
+
best.set(key, { entity, score, matched });
|
|
135
|
+
}
|
|
136
|
+
return [...best.values()]
|
|
137
|
+
.sort((a, b) => b.score - a.score || (a.entity.rel < b.entity.rel ? -1 : a.entity.rel > b.entity.rel ? 1 : 0))
|
|
138
|
+
.slice(0, ENTITY_HINTS.perInput)
|
|
139
|
+
.map(({ entity, matched }) => ({
|
|
140
|
+
type: entity.type,
|
|
141
|
+
name: entity.canonical_name,
|
|
142
|
+
// Aliases the text used come first, so the model sees the name it read.
|
|
143
|
+
aliases: [...matched, ...entity.aliases.filter((alias) => !matched.includes(alias))].slice(0, ENTITY_HINTS.aliases),
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=hints.js.map
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { type CompanyBriefResult } from "./company-brief.js";
|
|
11
11
|
import { type OntologyCompany } from "./company.js";
|
|
12
12
|
import { type MultiWriterStart } from "./multi-writer.js";
|
|
13
|
+
import { type EntityHint } from "./hints.js";
|
|
13
14
|
export declare const ONTOLOGY_FLAG = "checkpoint.ontology";
|
|
14
15
|
export declare const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
|
|
15
16
|
/**
|
|
@@ -31,6 +32,13 @@ export declare const GROK_BACKEND_FLAG = "checkpoint.ontology.grok-backend";
|
|
|
31
32
|
* duplicate entities.
|
|
32
33
|
*/
|
|
33
34
|
export declare const MULTI_WRITER_FLAG = "checkpoint.ontology.multi-writer";
|
|
35
|
+
/**
|
|
36
|
+
* Each knowledge input lists the existing entities its text names
|
|
37
|
+
* (`known_entities`), and the model is told to use those names and to skip
|
|
38
|
+
* table rows, paths and slugs. CO-41: 11 of 93 creates in the CO-39 grok-4.6
|
|
39
|
+
* runs were model errors, 5 of them duplicates of an existing entity.
|
|
40
|
+
*/
|
|
41
|
+
export declare const ENTITY_HINTS_FLAG = "checkpoint.ontology.entity-hints";
|
|
34
42
|
export declare const PLAN_FILE = "ontology-plan.json";
|
|
35
43
|
export declare const CANDIDATES_FILE = "ontology-candidates.json";
|
|
36
44
|
export declare const REPORT_FILE = "ontology-report.json";
|
|
@@ -80,6 +88,8 @@ export interface PlanInput {
|
|
|
80
88
|
missing_refs?: string[];
|
|
81
89
|
/** Run-directory-relative copy of `path` with annotator entity lists removed; same line numbers. */
|
|
82
90
|
read_path?: string;
|
|
91
|
+
/** Existing entities this input's text names; only when `checkpoint.ontology.entity-hints` is on. */
|
|
92
|
+
known_entities?: EntityHint[];
|
|
83
93
|
}
|
|
84
94
|
export interface OntologyPlan {
|
|
85
95
|
schema: typeof PLAN_SCHEMA;
|
|
@@ -138,6 +148,7 @@ export type PlanOutcome = {
|
|
|
138
148
|
companyBrief?: CompanyBriefResult;
|
|
139
149
|
};
|
|
140
150
|
export declare const READ_PATH_INSTRUCTION = "When an input has read_path, read that file (relative to the run directory) instead of path, with the same line range. It is the same text with an older annotator's entity lists and entity links removed.";
|
|
151
|
+
export declare const ENTITY_HINTS_INSTRUCTIONS: string[];
|
|
141
152
|
export declare function ledgerPath(hqRoot: string, company: OntologyCompany): string;
|
|
142
153
|
export declare function readLedger(hqRoot: string, company: OntologyCompany): Ledger | null;
|
|
143
154
|
export declare function writeLedger(hqRoot: string, company: OntologyCompany, ledger: Ledger): void;
|
|
@@ -13,6 +13,7 @@ import * as path from "node:path";
|
|
|
13
13
|
import { writeCompanyBrief } from "./company-brief.js";
|
|
14
14
|
import { resolveOntologyCompany, safeWriteFile, stateRoot } from "./company.js";
|
|
15
15
|
import { startMultiWriterRun } from "./multi-writer.js";
|
|
16
|
+
import { entityHints, wordWeights } from "./hints.js";
|
|
16
17
|
import { buildEntityIndex, listKnowledgeFiles, listSignalFiles, readSignal } from "./tree.js";
|
|
17
18
|
export const ONTOLOGY_FLAG = "checkpoint.ontology";
|
|
18
19
|
export const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
|
|
@@ -35,6 +36,13 @@ export const GROK_BACKEND_FLAG = "checkpoint.ontology.grok-backend";
|
|
|
35
36
|
* duplicate entities.
|
|
36
37
|
*/
|
|
37
38
|
export const MULTI_WRITER_FLAG = "checkpoint.ontology.multi-writer";
|
|
39
|
+
/**
|
|
40
|
+
* Each knowledge input lists the existing entities its text names
|
|
41
|
+
* (`known_entities`), and the model is told to use those names and to skip
|
|
42
|
+
* table rows, paths and slugs. CO-41: 11 of 93 creates in the CO-39 grok-4.6
|
|
43
|
+
* runs were model errors, 5 of them duplicates of an existing entity.
|
|
44
|
+
*/
|
|
45
|
+
export const ENTITY_HINTS_FLAG = "checkpoint.ontology.entity-hints";
|
|
38
46
|
export const PLAN_FILE = "ontology-plan.json";
|
|
39
47
|
export const CANDIDATES_FILE = "ontology-candidates.json";
|
|
40
48
|
export const REPORT_FILE = "ontology-report.json";
|
|
@@ -47,6 +55,10 @@ export const BUDGET = {
|
|
|
47
55
|
chunkTokens: 12_000,
|
|
48
56
|
};
|
|
49
57
|
export const READ_PATH_INSTRUCTION = "When an input has read_path, read that file (relative to the run directory) instead of path, with the same line range. It is the same text with an older annotator's entity lists and entity links removed.";
|
|
58
|
+
export const ENTITY_HINTS_INSTRUCTIONS = [
|
|
59
|
+
"A knowledge input's known_entities lists entities that already exist in the company and whose names appear in its text. When the input refers to one of them, under that name or another, use that entity's name and type exactly and do not name a new entity for it.",
|
|
60
|
+
"Skip rows of a glossary or translation table, file and directory paths, and names built from tags or slugs.",
|
|
61
|
+
];
|
|
50
62
|
const CANDIDATE_INSTRUCTIONS = [
|
|
51
63
|
`Write ${CANDIDATES_FILE} in the run directory as {"schema":"${CANDIDATES_SCHEMA}","candidates":[...]} and nothing else.`,
|
|
52
64
|
"Each candidate: {input, type, name, aliases, confidence, evidence}. input is one of this plan's input ids; type is person, project, company or concept; name is the entity's full proper name; aliases are other names the input uses for it (at most 5); confidence is 0 to 1; evidence is a short quote from the input.",
|
|
@@ -206,6 +218,7 @@ export async function planOntologyRun(options) {
|
|
|
206
218
|
const ignoreAnnotations = (await options.readFlag(IGNORE_ANNOTATIONS_FLAG, company)) && payload.backend === "grok";
|
|
207
219
|
recordBackendPreference(hqRoot, company, await options.readFlag(GROK_BACKEND_FLAG, company));
|
|
208
220
|
const multiWriterOn = await options.readFlag(MULTI_WRITER_FLAG, company);
|
|
221
|
+
const hintsOn = await options.readFlag(ENTITY_HINTS_FLAG, company);
|
|
209
222
|
const touched = sessionTouched(hqRoot, company, payload.files);
|
|
210
223
|
// A first run takes its boundary from the entity tree, so the ledger is
|
|
211
224
|
// refreshed before merge-back moves any `last_updated` forward.
|
|
@@ -218,6 +231,15 @@ export async function planOntologyRun(options) {
|
|
|
218
231
|
const pending = Object.entries(ledger.entries)
|
|
219
232
|
.filter(([, entry]) => entry.state === "pending" && entry.reason === "session-touched")
|
|
220
233
|
.sort(([aRel, a], [bRel, b]) => a.mtime_ms - b.mtime_ms || aRel.localeCompare(bRel));
|
|
234
|
+
// Built after merge-back, so merged entities are hinted too.
|
|
235
|
+
let hintIndex = null;
|
|
236
|
+
const hintsFor = (text) => {
|
|
237
|
+
if (!hintIndex) {
|
|
238
|
+
const index = buildEntityIndex(company.dir);
|
|
239
|
+
hintIndex = { index, weights: wordWeights(index) };
|
|
240
|
+
}
|
|
241
|
+
return entityHints(hintIndex.index, hintIndex.weights, text);
|
|
242
|
+
};
|
|
221
243
|
const inputs = [];
|
|
222
244
|
let tokens = 0;
|
|
223
245
|
let remaining = 0;
|
|
@@ -242,6 +264,10 @@ export async function planOntologyRun(options) {
|
|
|
242
264
|
const cost = estTokens(Buffer.byteLength(lines.slice(start - 1, end).join("\n"), "utf8"));
|
|
243
265
|
if (inputs.length >= BUDGET.maxInputs || tokens + cost > BUDGET.maxTokens)
|
|
244
266
|
break;
|
|
267
|
+
// The model reads the stripped copy when there is one; its line numbers match.
|
|
268
|
+
const knownEntities = hintsOn && kind === "knowledge"
|
|
269
|
+
? hintsFor((cleaned ?? text).split("\n").slice(start - 1, end).join("\n"))
|
|
270
|
+
: null;
|
|
245
271
|
inputs.push({
|
|
246
272
|
id: chunks.length > 1 ? `${rel}#${index}` : rel,
|
|
247
273
|
kind,
|
|
@@ -254,6 +280,7 @@ export async function planOntologyRun(options) {
|
|
|
254
280
|
chunk: chunks.length > 1 ? { index, count: chunks.length } : null,
|
|
255
281
|
...(kind === "signal" ? { missing_refs: missingRefs(company.dir, rel) } : {}),
|
|
256
282
|
...(readPath !== null ? { read_path: readPath } : {}),
|
|
283
|
+
...(knownEntities !== null ? { known_entities: knownEntities } : {}),
|
|
257
284
|
});
|
|
258
285
|
tokens += cost;
|
|
259
286
|
}
|
|
@@ -314,9 +341,13 @@ export async function planOntologyRun(options) {
|
|
|
314
341
|
planned_tokens: tokens,
|
|
315
342
|
},
|
|
316
343
|
candidates_file: CANDIDATES_FILE,
|
|
317
|
-
instructions:
|
|
318
|
-
|
|
319
|
-
:
|
|
344
|
+
instructions: [
|
|
345
|
+
...CANDIDATE_INSTRUCTIONS.slice(0, 3),
|
|
346
|
+
...(inputs.some((input) => input.read_path) ? [READ_PATH_INSTRUCTION] : []),
|
|
347
|
+
CANDIDATE_INSTRUCTIONS[3],
|
|
348
|
+
...(inputs.some((input) => input.known_entities) ? ENTITY_HINTS_INSTRUCTIONS : []),
|
|
349
|
+
...CANDIDATE_INSTRUCTIONS.slice(4),
|
|
350
|
+
],
|
|
320
351
|
};
|
|
321
352
|
fs.mkdirSync(runDir, { recursive: true });
|
|
322
353
|
const planPath = path.join(runDir, PLAN_FILE);
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -189,7 +189,7 @@ function selectSpawnAccount(input) {
|
|
|
189
189
|
const operatorMovePending = lane.pending_resume !== undefined &&
|
|
190
190
|
lane.account_history?.at(-1)?.reason === "operator_move";
|
|
191
191
|
if (current === undefined || !current.enabled) {
|
|
192
|
-
reason =
|
|
192
|
+
reason = "account_unavailable";
|
|
193
193
|
refusal = new AccountResolutionError(current === undefined ? "unknown_account" : "account_disabled", `Account ${JSON.stringify(lane.account)} is unavailable for ${lane.provider}`, { provider: lane.provider, account: lane.account });
|
|
194
194
|
}
|
|
195
195
|
else {
|
|
@@ -244,7 +244,11 @@ function selectSpawnAccount(input) {
|
|
|
244
244
|
account: lane.account,
|
|
245
245
|
});
|
|
246
246
|
}
|
|
247
|
-
|
|
247
|
+
const recoverRecordedClaudeModel = lane.provider === "claude" &&
|
|
248
|
+
reason !== undefined &&
|
|
249
|
+
(reason === "recorded_account_logged_out" ||
|
|
250
|
+
(reason === "account_unavailable" && repairRecordedAccount && recordedModel !== undefined));
|
|
251
|
+
if (recoverRecordedClaudeModel) {
|
|
248
252
|
const lanes = listLaneIds(hqRoot)
|
|
249
253
|
.map((laneId) => readLane(hqRoot, laneId))
|
|
250
254
|
.filter((record) => record.host.kind === lane.host.kind && record.host.id === lane.host.id);
|
|
@@ -268,7 +272,8 @@ function selectSpawnAccount(input) {
|
|
|
268
272
|
});
|
|
269
273
|
return {
|
|
270
274
|
account: resolveAccount(lane.provider, selection.account, config, context),
|
|
271
|
-
changeReason:
|
|
275
|
+
changeReason: reason,
|
|
276
|
+
...(recordedModel !== undefined ? { preservedModel: recordedModel } : {}),
|
|
272
277
|
};
|
|
273
278
|
}
|
|
274
279
|
const allowedAccounts = Object.entries(config.providers[lane.provider].accounts)
|
|
@@ -1726,7 +1731,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1726
1731
|
process.stderr.write(`hq lanes: account bind failed for ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1727
1732
|
throw err;
|
|
1728
1733
|
}
|
|
1729
|
-
const preservedAccountModel = selection.
|
|
1734
|
+
const preservedAccountModel = selection.preservedModel;
|
|
1730
1735
|
if (preservedAccountModel !== undefined)
|
|
1731
1736
|
pinnedModel = preservedAccountModel;
|
|
1732
1737
|
const resolved = selection.account;
|
|
@@ -523,7 +523,7 @@ export interface AccountHistoryEntry {
|
|
|
523
523
|
from: string | null;
|
|
524
524
|
to: string;
|
|
525
525
|
at: string;
|
|
526
|
-
reason: "first_assignment" | "model_change" | "usage_limit" | "account_unavailable" | "recorded_account_logged_out" | "operator_move";
|
|
526
|
+
reason: "first_assignment" | "model_change" | "usage_limit" | "account_unavailable" | "recorded_account_logged_out" | "load_balance" | "operator_move";
|
|
527
527
|
}
|
|
528
528
|
export interface PendingResume {
|
|
529
529
|
/** Fresh answered-question launches need no previous session id. */
|