shariq-pi-extensions 0.2.23 → 0.2.25
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/docs/EXTENSIONS.md +1 -1
- package/extensions/antigravity-provider/README.md +1 -1
- package/extensions/antigravity-provider/antigravity/accounts.ts +45 -6
- package/extensions/antigravity-provider/antigravity/dashboard.ts +11 -4
- package/extensions/antigravity-provider/antigravity/index.ts +52 -17
- package/extensions/factory-provider/API_KEYS.md +1 -1
- package/extensions/factory-provider/README.md +1 -1
- package/extensions/factory-provider/factory/api-keys.ts +63 -7
- package/extensions/factory-provider/factory/dashboard.ts +44 -12
- package/extensions/factory-provider/index.ts +47 -16
- package/extensions/task-list/README.md +2 -2
- package/extensions/task-list/index.ts +4 -44
- package/package.json +1 -1
package/docs/EXTENSIONS.md
CHANGED
|
@@ -36,7 +36,7 @@ The goal extension adds persistent, branch-safe objectives, progress evidence, b
|
|
|
36
36
|
|
|
37
37
|
### [Task List](../extensions/task-list/README.md)
|
|
38
38
|
|
|
39
|
-
`task_list` gives the active model a branch-safe ordered checklist for ordinary multi-step work; it is separate from persistent Goals and may be used alongside them. Writes replace the full list, retain stable IDs, support pending/in-progress/completed/blocked/cancelled states and priorities, and require an active item while pending work remains. The prompt contract requires same-message list/action calls and
|
|
39
|
+
`task_list` gives the active model a branch-safe ordered checklist for ordinary multi-step work; it is separate from persistent Goals and may be used alongside them. Writes replace the full list, retain stable IDs, support pending/in-progress/completed/blocked/cancelled states and priorities, and require an active item while pending work remains. The prompt contract requires same-message list/action calls and verified updates only at task-level transitions—not after routine file reads, edits, commands, or other tool calls. A runtime reminder catches multi-action work that starts without a list; final reconciliation catches unfinished bookkeeping.
|
|
40
40
|
|
|
41
41
|
State is stored in Pi session entries and reconstructed on resume, reload, and tree navigation. Active work is re-injected when compaction removes the latest snapshot from model context. `/tasks` opens the interactive dashboard and direct editor; the compact live widget auto-hides after all work finishes. Subagents receive the same tool under every capability policy, but each child maintains its own session-local list rather than changing the parent's list.
|
|
42
42
|
|
|
@@ -10,7 +10,7 @@ Local persistent Pi provider for Google Antigravity-compatible models.
|
|
|
10
10
|
|
|
11
11
|
The provider includes an IPv4 OAuth token-exchange fallback for Node environments where the default request fails. Its curated catalog follows current Antigravity model identifiers and runtime behavior.
|
|
12
12
|
|
|
13
|
-
Successful logins are added to `<agent-dir>/antigravity/accounts.json`, written with owner-only permissions.
|
|
13
|
+
Successful logins are added to `<agent-dir>/antigravity/accounts.json`, written with owner-only permissions. A missing account file triggers one-time migration of an existing Pi OAuth credential; after that, the valid account file is authoritative, so removing an account cannot be undone by a stale credential in `auth.json`. Requests select the least recently used eligible account, skip disabled/cooling/exhausted accounts, refresh expiring OAuth tokens, and rotate to another account when an auth, rate, quota, or capacity failure occurs before response streaming begins. Cached per-model remaining quota and reset times guide selection; `/antigravity` refreshes the authoritative catalog, uses `d` to reversibly enable or disable an account, and uses `x` plus confirmation to remove one permanently.
|
|
14
14
|
|
|
15
15
|
Current public model IDs:
|
|
16
16
|
- `antigravity/gemini-3.7-flash`
|
|
@@ -48,6 +48,22 @@ interface AntigravityAccountFile {
|
|
|
48
48
|
accounts: AntigravityAccount[];
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export type AntigravityAccountStore = {
|
|
52
|
+
status: "missing" | "invalid" | "valid";
|
|
53
|
+
accounts: AntigravityAccount[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type AntigravityStoredCredential = Pick<OAuthCredentials, "refresh" | "access" | "expires"> & {
|
|
57
|
+
projectId?: string;
|
|
58
|
+
email?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type AntigravityCredentialReconciliation =
|
|
62
|
+
| { action: "none" }
|
|
63
|
+
| { action: "migrate"; credentials: AntigravityStoredCredential }
|
|
64
|
+
| { action: "replace"; account: AntigravityAccount }
|
|
65
|
+
| { action: "delete" };
|
|
66
|
+
|
|
51
67
|
export interface AntigravityAccountStatus extends AntigravityAccount {
|
|
52
68
|
active: boolean;
|
|
53
69
|
}
|
|
@@ -105,17 +121,40 @@ function normalizeAccount(value: unknown): AntigravityAccount | undefined {
|
|
|
105
121
|
};
|
|
106
122
|
}
|
|
107
123
|
|
|
108
|
-
export function
|
|
124
|
+
export function inspectAntigravityAccountStore(): AntigravityAccountStore {
|
|
109
125
|
try {
|
|
110
126
|
const parsed = JSON.parse(fs.readFileSync(ANTIGRAVITY_ACCOUNTS_PATH, "utf8")) as Partial<AntigravityAccountFile>;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
:
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
if (!Array.isArray(parsed.accounts)) return { status: "invalid", accounts: [] };
|
|
128
|
+
return {
|
|
129
|
+
status: "valid",
|
|
130
|
+
accounts: parsed.accounts.map(normalizeAccount).filter((account): account is AntigravityAccount => Boolean(account)),
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
134
|
+
return { status: code === "ENOENT" ? "missing" : "invalid", accounts: [] };
|
|
116
135
|
}
|
|
117
136
|
}
|
|
118
137
|
|
|
138
|
+
export function loadAntigravityAccounts(): AntigravityAccount[] {
|
|
139
|
+
return inspectAntigravityAccountStore().accounts;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function matchesStoredCredential(account: AntigravityAccount, credentials: AntigravityStoredCredential) {
|
|
143
|
+
return account.refresh === credentials.refresh || Boolean(account.email && credentials.email && account.email === credentials.email);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function reconcileAntigravityStoredCredential(
|
|
147
|
+
store: AntigravityAccountStore,
|
|
148
|
+
credentials: AntigravityStoredCredential | undefined,
|
|
149
|
+
): AntigravityCredentialReconciliation {
|
|
150
|
+
if (store.status === "invalid") return { action: "none" };
|
|
151
|
+
if (store.status === "missing") return credentials ? { action: "migrate", credentials } : { action: "none" };
|
|
152
|
+
if (credentials && store.accounts.some((account) => matchesStoredCredential(account, credentials))) return { action: "none" };
|
|
153
|
+
const replacement = store.accounts.find((account) => !account.disabled) ?? store.accounts[0];
|
|
154
|
+
if (replacement) return { action: "replace", account: replacement };
|
|
155
|
+
return credentials ? { action: "delete" } : { action: "none" };
|
|
156
|
+
}
|
|
157
|
+
|
|
119
158
|
function saveAntigravityAccounts(accounts: AntigravityAccount[]) {
|
|
120
159
|
fs.mkdirSync(ANTIGRAVITY_STATE_DIR, { recursive: true, mode: 0o700 });
|
|
121
160
|
fs.chmodSync(ANTIGRAVITY_STATE_DIR, 0o700);
|
|
@@ -78,6 +78,7 @@ export class AntigravityDashboard implements Component {
|
|
|
78
78
|
private snapshot: AntigravityDashboardSnapshot;
|
|
79
79
|
private readonly refreshData: (force: boolean) => Promise<AntigravityDashboardSnapshot>;
|
|
80
80
|
private readonly toggleAccount: (id: string, enabled: boolean) => Promise<AntigravityDashboardSnapshot>;
|
|
81
|
+
private readonly removeAccount: (id: string, label: string) => Promise<AntigravityDashboardSnapshot>;
|
|
81
82
|
private readonly done: () => void;
|
|
82
83
|
|
|
83
84
|
constructor(
|
|
@@ -87,6 +88,7 @@ export class AntigravityDashboard implements Component {
|
|
|
87
88
|
snapshot: AntigravityDashboardSnapshot,
|
|
88
89
|
refreshData: (force: boolean) => Promise<AntigravityDashboardSnapshot>,
|
|
89
90
|
toggleAccount: (id: string, enabled: boolean) => Promise<AntigravityDashboardSnapshot>,
|
|
91
|
+
removeAccount: (id: string, label: string) => Promise<AntigravityDashboardSnapshot>,
|
|
90
92
|
done: () => void,
|
|
91
93
|
) {
|
|
92
94
|
this.tui = tui;
|
|
@@ -95,6 +97,7 @@ export class AntigravityDashboard implements Component {
|
|
|
95
97
|
this.snapshot = snapshot;
|
|
96
98
|
this.refreshData = refreshData;
|
|
97
99
|
this.toggleAccount = toggleAccount;
|
|
100
|
+
this.removeAccount = removeAccount;
|
|
98
101
|
this.done = done;
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -139,10 +142,13 @@ export class AntigravityDashboard implements Component {
|
|
|
139
142
|
if (accounts.length) this.selected = (this.selected + 1) % accounts.length;
|
|
140
143
|
} else if (data === "r") {
|
|
141
144
|
this.startRefresh(true);
|
|
142
|
-
} else if (data === "d" && accounts[this.selected] && !this.refreshing) {
|
|
145
|
+
} else if ((data === "d" || data === "x") && accounts[this.selected] && !this.refreshing) {
|
|
143
146
|
const account = accounts[this.selected]!;
|
|
144
147
|
this.refreshing = true;
|
|
145
|
-
|
|
148
|
+
const action = data === "x"
|
|
149
|
+
? this.removeAccount(account.id, account.email || `account-${account.id.slice(0, 6)}`)
|
|
150
|
+
: this.toggleAccount(account.id, account.disabled === true);
|
|
151
|
+
void action
|
|
146
152
|
.then((snapshot) => this.replaceSnapshot(snapshot))
|
|
147
153
|
.catch((error) => {
|
|
148
154
|
if (!this.closed) this.snapshot = { ...this.snapshot, warning: error instanceof Error ? error.message : String(error) };
|
|
@@ -188,7 +194,7 @@ export class AntigravityDashboard implements Component {
|
|
|
188
194
|
for (let row = 0; row < bodyHeight; row++) lines.push(this.theme.fg("border", "│") + padLine(list[row] ?? "", inner) + this.theme.fg("border", "│"));
|
|
189
195
|
}
|
|
190
196
|
lines.push(frameBottom(this.theme, width));
|
|
191
|
-
lines.push(truncateToWidth(`${this.theme.fg("accent", " ↑↓ / j k")} ${this.theme.fg("dim", "select")} ${this.theme.fg("accent", "r")} ${this.theme.fg("dim", "refresh")} ${this.theme.fg("accent", "d")} ${this.theme.fg("dim", "enable/disable")} ${this.theme.fg("accent", "esc")} ${this.theme.fg("dim", "close")}`, width, ""));
|
|
197
|
+
lines.push(truncateToWidth(`${this.theme.fg("accent", " ↑↓ / j k")} ${this.theme.fg("dim", "select")} ${this.theme.fg("accent", "r")} ${this.theme.fg("dim", "refresh")} ${this.theme.fg("accent", "d")} ${this.theme.fg("dim", "enable/disable")} ${this.theme.fg("accent", "x")} ${this.theme.fg("dim", "remove")} ${this.theme.fg("accent", "esc")} ${this.theme.fg("dim", "close")}`, width, ""));
|
|
192
198
|
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
193
199
|
}
|
|
194
200
|
|
|
@@ -250,10 +256,11 @@ export async function openAntigravityDashboard(
|
|
|
250
256
|
initial: AntigravityDashboardSnapshot,
|
|
251
257
|
refresh: (force: boolean) => Promise<AntigravityDashboardSnapshot>,
|
|
252
258
|
toggle: (id: string, enabled: boolean) => Promise<AntigravityDashboardSnapshot>,
|
|
259
|
+
remove: (id: string, label: string) => Promise<AntigravityDashboardSnapshot>,
|
|
253
260
|
) {
|
|
254
261
|
await ctx.ui.custom<void>(
|
|
255
262
|
(tui, theme, keys, done) => {
|
|
256
|
-
const dashboard = new AntigravityDashboard(tui, theme, keys, initial, refresh, toggle, () => done(undefined));
|
|
263
|
+
const dashboard = new AntigravityDashboard(tui, theme, keys, initial, refresh, toggle, remove, () => done(undefined));
|
|
257
264
|
queueMicrotask(() => dashboard.startRefresh(false));
|
|
258
265
|
return dashboard;
|
|
259
266
|
},
|
|
@@ -24,8 +24,12 @@ import {
|
|
|
24
24
|
import { streamAntigravity } from "./cloud-code-assist.ts";
|
|
25
25
|
import {
|
|
26
26
|
antigravityAccountStatuses,
|
|
27
|
+
inspectAntigravityAccountStore,
|
|
28
|
+
reconcileAntigravityStoredCredential,
|
|
29
|
+
removeAntigravityAccount,
|
|
27
30
|
setAntigravityAccountEnabled,
|
|
28
31
|
upsertAntigravityAccount,
|
|
32
|
+
type AntigravityStoredCredential,
|
|
29
33
|
} from "./accounts.ts";
|
|
30
34
|
import { refreshAntigravityQuotas } from "./quotas.ts";
|
|
31
35
|
import {
|
|
@@ -43,19 +47,43 @@ export default function antigravityProviderExtension(pi: ExtensionAPI) {
|
|
|
43
47
|
};
|
|
44
48
|
};
|
|
45
49
|
|
|
46
|
-
const
|
|
50
|
+
const readOAuthCredential = (): AntigravityStoredCredential | undefined => {
|
|
51
|
+
const credential = readStoredCredential(PROVIDER_ID) as any;
|
|
52
|
+
if (credential?.type !== "oauth" || typeof credential.refresh !== "string" || typeof credential.access !== "string") return undefined;
|
|
53
|
+
return {
|
|
54
|
+
refresh: credential.refresh,
|
|
55
|
+
access: credential.access,
|
|
56
|
+
expires: typeof credential.expires === "number" ? credential.expires : 0,
|
|
57
|
+
projectId: typeof credential.projectId === "string" ? credential.projectId : undefined,
|
|
58
|
+
email: typeof credential.email === "string" ? credential.email : undefined,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const reconcileStoredCredential = async (ctx?: ExtensionContext) => {
|
|
47
63
|
try {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
const credentials = readOAuthCredential();
|
|
65
|
+
const reconciliation = reconcileAntigravityStoredCredential(inspectAntigravityAccountStore(), credentials);
|
|
66
|
+
if (reconciliation.action === "migrate") {
|
|
67
|
+
upsertAntigravityAccount(reconciliation.credentials);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const authStorage = (ctx?.modelRegistry as any)?.authStorage;
|
|
71
|
+
if (!authStorage) return;
|
|
72
|
+
if (reconciliation.action === "replace") {
|
|
73
|
+
const account = reconciliation.account;
|
|
74
|
+
await authStorage.modify(PROVIDER_ID, async () => ({
|
|
75
|
+
type: "oauth",
|
|
76
|
+
refresh: account.refresh,
|
|
77
|
+
access: account.access,
|
|
78
|
+
expires: account.expires,
|
|
79
|
+
projectId: account.projectId,
|
|
80
|
+
email: account.email,
|
|
81
|
+
}));
|
|
82
|
+
} else if (reconciliation.action === "delete") {
|
|
83
|
+
await authStorage.delete(PROVIDER_ID);
|
|
84
|
+
}
|
|
57
85
|
} catch {
|
|
58
|
-
//
|
|
86
|
+
// Malformed or unavailable credential state must not block extension startup.
|
|
59
87
|
}
|
|
60
88
|
};
|
|
61
89
|
|
|
@@ -67,9 +95,9 @@ export default function antigravityProviderExtension(pi: ExtensionAPI) {
|
|
|
67
95
|
oauth: {
|
|
68
96
|
name: PROVIDER_NAME,
|
|
69
97
|
login: ((callbacks: Parameters<typeof loginAntigravity>[0]) => {
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
98
|
+
// A missing account store is a legacy installation, so archive Pi's single
|
|
99
|
+
// credential before login. A valid store remains authoritative after edits.
|
|
100
|
+
void reconcileStoredCredential();
|
|
73
101
|
return loginAntigravity(callbacks);
|
|
74
102
|
}) as any,
|
|
75
103
|
refreshToken: refreshAntigravityToken as any,
|
|
@@ -78,9 +106,9 @@ export default function antigravityProviderExtension(pi: ExtensionAPI) {
|
|
|
78
106
|
streamSimple: streamAntigravity,
|
|
79
107
|
} as any);
|
|
80
108
|
|
|
81
|
-
pi.on("session_start", (_event, ctx) => {
|
|
109
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
82
110
|
(ctx.modelRegistry as any).authStorage?.reload?.();
|
|
83
|
-
|
|
111
|
+
await reconcileStoredCredential(ctx);
|
|
84
112
|
void refreshAntigravityQuotas({ signal: ctx.signal }).catch(() => {
|
|
85
113
|
// Cached quota state remains available; /antigravity reports refresh failures.
|
|
86
114
|
});
|
|
@@ -94,7 +122,7 @@ export default function antigravityProviderExtension(pi: ExtensionAPI) {
|
|
|
94
122
|
pi.registerCommand("antigravity", {
|
|
95
123
|
description: "Open Antigravity accounts, rotation, and quota dashboard",
|
|
96
124
|
handler: async (_args, ctx) => {
|
|
97
|
-
|
|
125
|
+
await reconcileStoredCredential(ctx);
|
|
98
126
|
await openAntigravityDashboard(
|
|
99
127
|
ctx,
|
|
100
128
|
dashboardSnapshot(ctx),
|
|
@@ -107,6 +135,13 @@ export default function antigravityProviderExtension(pi: ExtensionAPI) {
|
|
|
107
135
|
if (enabled) await refreshAntigravityQuotas({ force: true, signal: ctx.signal });
|
|
108
136
|
return dashboardSnapshot(ctx);
|
|
109
137
|
},
|
|
138
|
+
async (id, label) => {
|
|
139
|
+
const confirmed = await ctx.ui.confirm("Remove Antigravity account?", `Permanently remove ${label} from this Pi installation?`);
|
|
140
|
+
if (!confirmed) return dashboardSnapshot(ctx);
|
|
141
|
+
removeAntigravityAccount(id);
|
|
142
|
+
await reconcileStoredCredential(ctx);
|
|
143
|
+
return dashboardSnapshot(ctx);
|
|
144
|
+
},
|
|
110
145
|
);
|
|
111
146
|
},
|
|
112
147
|
});
|
|
@@ -14,4 +14,4 @@ Alternatively choose Pi's top-level **Use an API key** authentication method to
|
|
|
14
14
|
|
|
15
15
|
Use `/logout` to remove the active Factory authentication selection. The configured key file is intentionally not deleted by logout.
|
|
16
16
|
|
|
17
|
-
Open `/factory` to inspect every account, its Standard/Core usage, and rotation status. For a smoke test, use the current `factory/kimi-k3` model.
|
|
17
|
+
Open `/factory` to inspect every account, its Standard/Core usage, and rotation status. File-backed keys remain visible when disabled: select one and press `d` to enable/disable it, or press `x` and confirm to remove it permanently from `api-keys.json`. Environment-provided keys are read-only. For a smoke test, use the current `factory/kimi-k3` model.
|
|
@@ -59,7 +59,7 @@ There are no `-oauth` or `-api-key` model duplicates. The user-curated catalog e
|
|
|
59
59
|
/factory
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
`/factory` opens a full-width account dashboard combining provider status, authentication, Droid/model metadata, rotation cooldowns, and separate Standard and Droid Core usage for every credential. Percentages are explicitly labeled as **used**. Navigate with the arrow keys or `j`/`k`, press `r` to force-refresh every account, and press Escape to close.
|
|
62
|
+
`/factory` opens a full-width account dashboard combining provider status, authentication, Droid/model metadata, rotation cooldowns, and separate Standard and Droid Core usage for every credential. Percentages are explicitly labeled as **used**. Navigate with the arrow keys or `j`/`k`, press `r` to force-refresh every account, use `d` to reversibly enable or disable a rotating file-backed key, use `x` plus confirmation to remove one permanently, and press Escape to close. Environment variables and single credentials managed by Pi auth remain read-only in this dashboard.
|
|
63
63
|
|
|
64
64
|
Usage is never estimated from Pi token counts: Factory's API already applies model multipliers and cache-hit discounts. Cached records refresh at most every 15 minutes during normal sessions and after Factory runs; the manual command is the explicit force-refresh path.
|
|
65
65
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
5
5
|
import { FACTORY_API_BASE_URL, FACTORY_STATE_DIR } from "./constants.ts";
|
|
@@ -25,6 +25,10 @@ export type FactoryApiKeyEntry = {
|
|
|
25
25
|
disabled?: boolean;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
type FactoryApiKeySourceEntry = FactoryApiKeyEntry & {
|
|
29
|
+
source: "environment" | "file";
|
|
30
|
+
};
|
|
31
|
+
|
|
28
32
|
type FactoryApiKeyConfig = {
|
|
29
33
|
keys?: Array<string | Partial<FactoryApiKeyEntry>>;
|
|
30
34
|
};
|
|
@@ -69,19 +73,19 @@ export function parseFactoryApiKeyFile(raw: string): { entries: FactoryApiKeyEnt
|
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
const entries:
|
|
76
|
+
function configuredFactoryApiKeys(): FactoryApiKeySourceEntry[] {
|
|
77
|
+
const entries: FactoryApiKeySourceEntry[] = [];
|
|
74
78
|
const envKeys = (process.env.FACTORY_API_KEYS || process.env.FACTORY_API_KEY || "")
|
|
75
79
|
.split(/[\n,]+/)
|
|
76
80
|
.map((key) => key.trim())
|
|
77
81
|
.filter(Boolean);
|
|
78
|
-
envKeys.forEach((key, index) => entries.push({ label: `env-${index + 1}`, key }));
|
|
82
|
+
envKeys.forEach((key, index) => entries.push({ label: `env-${index + 1}`, key, source: "environment" }));
|
|
79
83
|
|
|
80
84
|
lastConfigurationWarning = undefined;
|
|
81
85
|
if (existsSync(FACTORY_API_KEYS_PATH)) {
|
|
82
86
|
try {
|
|
83
87
|
const parsed = parseFactoryApiKeyFile(readFileSync(FACTORY_API_KEYS_PATH, "utf8"));
|
|
84
|
-
entries.push(...parsed.entries);
|
|
88
|
+
entries.push(...parsed.entries.map((entry) => ({ ...entry, source: "file" as const })));
|
|
85
89
|
lastConfigurationWarning = parsed.warning;
|
|
86
90
|
} catch {
|
|
87
91
|
lastConfigurationWarning = "Factory rotating-key configuration could not be read.";
|
|
@@ -90,13 +94,63 @@ export function loadFactoryApiKeys(): FactoryApiKeyEntry[] {
|
|
|
90
94
|
|
|
91
95
|
const seen = new Set<string>();
|
|
92
96
|
return entries.filter((entry) => {
|
|
93
|
-
if (entry.disabled) return false;
|
|
94
97
|
if (seen.has(entry.key)) return false;
|
|
95
98
|
seen.add(entry.key);
|
|
96
99
|
return true;
|
|
97
100
|
});
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
export function loadFactoryApiKeys(): FactoryApiKeyEntry[] {
|
|
104
|
+
return configuredFactoryApiKeys().filter((entry) => !entry.disabled);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function editableFactoryApiKeyEntries(): FactoryApiKeyEntry[] {
|
|
108
|
+
if (!existsSync(FACTORY_API_KEYS_PATH)) return [];
|
|
109
|
+
const parsed = parseFactoryApiKeyFile(readFileSync(FACTORY_API_KEYS_PATH, "utf8"));
|
|
110
|
+
if (parsed.warning) throw new Error(parsed.warning);
|
|
111
|
+
return parsed.entries;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function saveFactoryApiKeyEntries(entries: FactoryApiKeyEntry[]) {
|
|
115
|
+
mkdirSync(FACTORY_STATE_DIR, { recursive: true, mode: 0o700 });
|
|
116
|
+
chmodSync(FACTORY_STATE_DIR, 0o700);
|
|
117
|
+
const temporary = `${FACTORY_API_KEYS_PATH}.${process.pid}.tmp`;
|
|
118
|
+
writeFileSync(temporary, `${JSON.stringify({ keys: entries }, null, 2)}\n`, { mode: 0o600 });
|
|
119
|
+
renameSync(temporary, FACTORY_API_KEYS_PATH);
|
|
120
|
+
chmodSync(FACTORY_API_KEYS_PATH, 0o600);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function updateFactoryApiKeyEntries(
|
|
124
|
+
entries: FactoryApiKeyEntry[],
|
|
125
|
+
id: string,
|
|
126
|
+
action: { kind: "enabled"; enabled: boolean } | { kind: "remove" },
|
|
127
|
+
) {
|
|
128
|
+
let changed = false;
|
|
129
|
+
const next = entries.flatMap((entry): FactoryApiKeyEntry[] => {
|
|
130
|
+
if (keyId(entry) !== id) return [entry];
|
|
131
|
+
changed = true;
|
|
132
|
+
if (action.kind === "remove") return [];
|
|
133
|
+
return [{ ...entry, disabled: !action.enabled }];
|
|
134
|
+
});
|
|
135
|
+
return { entries: next, changed };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function setFactoryApiKeyEnabled(id: string, enabled: boolean) {
|
|
139
|
+
const result = updateFactoryApiKeyEntries(editableFactoryApiKeyEntries(), id, { kind: "enabled", enabled });
|
|
140
|
+
if (!result.changed) return false;
|
|
141
|
+
saveFactoryApiKeyEntries(result.entries);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function removeFactoryApiKey(id: string) {
|
|
146
|
+
const result = updateFactoryApiKeyEntries(editableFactoryApiKeyEntries(), id, { kind: "remove" });
|
|
147
|
+
if (!result.changed) return false;
|
|
148
|
+
saveFactoryApiKeyEntries(result.entries);
|
|
149
|
+
state.delete(id);
|
|
150
|
+
organizationIdCache.delete(id);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
100
154
|
export function sortFactoryApiKeysByLastUsed(
|
|
101
155
|
entries: FactoryApiKeyEntry[],
|
|
102
156
|
lastUsed = (entry: FactoryApiKeyEntry) => state.get(keyId(entry))?.lastUsedAt,
|
|
@@ -395,7 +449,7 @@ export function streamSimpleUnifiedFactoryResponses(model: any, context: any, op
|
|
|
395
449
|
}
|
|
396
450
|
|
|
397
451
|
export function factoryApiKeyStatus() {
|
|
398
|
-
const keys =
|
|
452
|
+
const keys = configuredFactoryApiKeys();
|
|
399
453
|
const now = Date.now();
|
|
400
454
|
return {
|
|
401
455
|
configured: keys.length,
|
|
@@ -408,6 +462,8 @@ export function factoryApiKeyStatus() {
|
|
|
408
462
|
id: keyId(entry),
|
|
409
463
|
label: entry.label,
|
|
410
464
|
key: maskKey(entry.key),
|
|
465
|
+
disabled: Boolean(entry.disabled),
|
|
466
|
+
editable: entry.source === "file",
|
|
411
467
|
cooldownSeconds: runtime?.cooldownUntil && runtime.cooldownUntil > now ? Math.ceil((runtime.cooldownUntil - now) / 1000) : 0,
|
|
412
468
|
lastError: runtime?.lastError ? runtime.lastError.slice(0, 200) : undefined,
|
|
413
469
|
lastUsedAt: runtime?.lastUsedAt ? new Date(runtime.lastUsedAt).toISOString() : undefined,
|
|
@@ -26,6 +26,8 @@ import { factoryWarmTheme } from "./warm-theme.ts";
|
|
|
26
26
|
export interface FactoryDashboardAccount {
|
|
27
27
|
id: string;
|
|
28
28
|
label: string;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
editable?: boolean;
|
|
29
31
|
record?: FactoryLimitRecord;
|
|
30
32
|
cooldownSeconds?: number;
|
|
31
33
|
lastUsedAt?: string;
|
|
@@ -96,6 +98,7 @@ function compactPool(
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
function accountState(account: FactoryDashboardAccount) {
|
|
101
|
+
if (account.disabled) return "disabled";
|
|
99
102
|
if (account.cooldownSeconds && account.cooldownSeconds > 0) return "cooldown";
|
|
100
103
|
if (account.record?.error) return "refresh error";
|
|
101
104
|
const observedAt = account.record?.fetchedAt ?? Date.now();
|
|
@@ -122,6 +125,8 @@ export class FactoryDashboard implements Component {
|
|
|
122
125
|
private readonly keys: KeybindingsManager;
|
|
123
126
|
private snapshot: FactoryDashboardSnapshot;
|
|
124
127
|
private readonly refreshData: (force: boolean) => Promise<FactoryDashboardSnapshot>;
|
|
128
|
+
private readonly toggleAccount: (id: string, enabled: boolean) => Promise<FactoryDashboardSnapshot>;
|
|
129
|
+
private readonly removeAccount: (id: string, label: string) => Promise<FactoryDashboardSnapshot>;
|
|
125
130
|
private readonly done: () => void;
|
|
126
131
|
|
|
127
132
|
constructor(
|
|
@@ -130,6 +135,8 @@ export class FactoryDashboard implements Component {
|
|
|
130
135
|
keys: KeybindingsManager,
|
|
131
136
|
snapshot: FactoryDashboardSnapshot,
|
|
132
137
|
refreshData: (force: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
138
|
+
toggleAccount: (id: string, enabled: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
139
|
+
removeAccount: (id: string, label: string) => Promise<FactoryDashboardSnapshot>,
|
|
133
140
|
done: () => void,
|
|
134
141
|
) {
|
|
135
142
|
this.tui = tui;
|
|
@@ -137,6 +144,8 @@ export class FactoryDashboard implements Component {
|
|
|
137
144
|
this.keys = keys;
|
|
138
145
|
this.snapshot = snapshot;
|
|
139
146
|
this.refreshData = refreshData;
|
|
147
|
+
this.toggleAccount = toggleAccount;
|
|
148
|
+
this.removeAccount = removeAccount;
|
|
140
149
|
this.done = done;
|
|
141
150
|
}
|
|
142
151
|
|
|
@@ -145,17 +154,7 @@ export class FactoryDashboard implements Component {
|
|
|
145
154
|
this.refreshing = true;
|
|
146
155
|
this.tui.requestRender();
|
|
147
156
|
void this.refreshData(force)
|
|
148
|
-
.then((snapshot) =>
|
|
149
|
-
if (this.closed) return;
|
|
150
|
-
const selectedId = this.snapshot.accounts[this.selected]?.id;
|
|
151
|
-
this.snapshot = snapshot;
|
|
152
|
-
const nextIndex = selectedId
|
|
153
|
-
? snapshot.accounts.findIndex((account) => account.id === selectedId)
|
|
154
|
-
: -1;
|
|
155
|
-
this.selected = nextIndex >= 0
|
|
156
|
-
? nextIndex
|
|
157
|
-
: Math.min(this.selected, Math.max(0, snapshot.accounts.length - 1));
|
|
158
|
-
})
|
|
157
|
+
.then((snapshot) => this.replaceSnapshot(snapshot))
|
|
159
158
|
.catch((error) => {
|
|
160
159
|
if (this.closed) return;
|
|
161
160
|
this.snapshot = {
|
|
@@ -170,6 +169,18 @@ export class FactoryDashboard implements Component {
|
|
|
170
169
|
});
|
|
171
170
|
}
|
|
172
171
|
|
|
172
|
+
private replaceSnapshot(snapshot: FactoryDashboardSnapshot) {
|
|
173
|
+
if (this.closed) return;
|
|
174
|
+
const selectedId = this.snapshot.accounts[this.selected]?.id;
|
|
175
|
+
this.snapshot = snapshot;
|
|
176
|
+
const nextIndex = selectedId
|
|
177
|
+
? snapshot.accounts.findIndex((account) => account.id === selectedId)
|
|
178
|
+
: -1;
|
|
179
|
+
this.selected = nextIndex >= 0
|
|
180
|
+
? nextIndex
|
|
181
|
+
: Math.min(this.selected, Math.max(0, snapshot.accounts.length - 1));
|
|
182
|
+
}
|
|
183
|
+
|
|
173
184
|
dispose() {
|
|
174
185
|
this.closed = true;
|
|
175
186
|
}
|
|
@@ -191,6 +202,23 @@ export class FactoryDashboard implements Component {
|
|
|
191
202
|
if (accounts.length) this.selected = (this.selected + 1) % accounts.length;
|
|
192
203
|
} else if (data === "r") {
|
|
193
204
|
this.startRefresh(true);
|
|
205
|
+
} else if ((data === "d" || data === "x") && accounts[this.selected]?.editable && !this.refreshing) {
|
|
206
|
+
const account = accounts[this.selected]!;
|
|
207
|
+
this.refreshing = true;
|
|
208
|
+
const action = data === "x"
|
|
209
|
+
? this.removeAccount(account.id, account.label)
|
|
210
|
+
: this.toggleAccount(account.id, account.disabled === true);
|
|
211
|
+
void action
|
|
212
|
+
.then((snapshot) => this.replaceSnapshot(snapshot))
|
|
213
|
+
.catch((error) => {
|
|
214
|
+
if (!this.closed) this.snapshot = { ...this.snapshot, warning: error instanceof Error ? error.message : String(error) };
|
|
215
|
+
})
|
|
216
|
+
.finally(() => {
|
|
217
|
+
if (!this.closed) {
|
|
218
|
+
this.refreshing = false;
|
|
219
|
+
this.tui.requestRender();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
194
222
|
}
|
|
195
223
|
this.tui.requestRender();
|
|
196
224
|
}
|
|
@@ -242,7 +270,7 @@ export class FactoryDashboard implements Component {
|
|
|
242
270
|
lines.push(frameBottom(this.theme, width));
|
|
243
271
|
lines.push(
|
|
244
272
|
truncateToWidth(
|
|
245
|
-
`${this.theme.fg("accent", " ↑↓ / j k")} ${this.theme.fg("dim", "select")} ${this.theme.fg("accent", "r")} ${this.theme.fg("dim", "refresh all")} ${this.theme.fg("accent", "esc")} ${this.theme.fg("dim", "close")}`,
|
|
273
|
+
`${this.theme.fg("accent", " ↑↓ / j k")} ${this.theme.fg("dim", "select")} ${this.theme.fg("accent", "r")} ${this.theme.fg("dim", "refresh all")} ${this.theme.fg("accent", "d")} ${this.theme.fg("dim", "enable/disable")} ${this.theme.fg("accent", "x")} ${this.theme.fg("dim", "remove")} ${this.theme.fg("accent", "esc")} ${this.theme.fg("dim", "close")}`,
|
|
246
274
|
width,
|
|
247
275
|
"",
|
|
248
276
|
),
|
|
@@ -341,6 +369,8 @@ export async function openFactoryDashboard(
|
|
|
341
369
|
ctx: ExtensionContext,
|
|
342
370
|
initial: FactoryDashboardSnapshot,
|
|
343
371
|
refresh: (force: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
372
|
+
toggle: (id: string, enabled: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
373
|
+
remove: (id: string, label: string) => Promise<FactoryDashboardSnapshot>,
|
|
344
374
|
) {
|
|
345
375
|
await ctx.ui.custom<void>(
|
|
346
376
|
(tui, theme, keys, done) => {
|
|
@@ -350,6 +380,8 @@ export async function openFactoryDashboard(
|
|
|
350
380
|
keys,
|
|
351
381
|
initial,
|
|
352
382
|
refresh,
|
|
383
|
+
toggle,
|
|
384
|
+
remove,
|
|
353
385
|
() => done(undefined),
|
|
354
386
|
);
|
|
355
387
|
queueMicrotask(() => dashboard.startRefresh(false));
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
FACTORY_API_KEY_FILE_SENTINEL,
|
|
12
12
|
factoryApiKeyStatus,
|
|
13
13
|
loadFactoryApiKeys,
|
|
14
|
+
removeFactoryApiKey,
|
|
15
|
+
setFactoryApiKeyEnabled,
|
|
14
16
|
streamSimpleUnifiedFactoryResponses,
|
|
15
17
|
} from "./factory/api-keys.ts";
|
|
16
18
|
import {
|
|
@@ -88,21 +90,35 @@ export default async function factoryExtension(pi: ExtensionAPI) {
|
|
|
88
90
|
ctx: ExtensionContext,
|
|
89
91
|
): Promise<FactoryDashboardSnapshot> => {
|
|
90
92
|
const credentials = await limitCredentials(ctx);
|
|
93
|
+
const resolved = await ctx.modelRegistry.getProviderAuth(PROVIDER_ID);
|
|
94
|
+
const fileBacked = resolved?.auth.apiKey?.trim() === FACTORY_API_KEY_FILE_SENTINEL;
|
|
91
95
|
const auth = ctx.modelRegistry.getProviderAuthStatus(PROVIDER_ID);
|
|
92
96
|
const apiKeys = factoryApiKeyStatus();
|
|
93
97
|
const cache = loadFactoryLimitCache();
|
|
94
98
|
const runtimeById = new Map(apiKeys.keys.map((key) => [key.id, key]));
|
|
95
|
-
const accounts =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
const accounts = fileBacked
|
|
100
|
+
? apiKeys.keys.map((key) => ({
|
|
101
|
+
id: key.id,
|
|
102
|
+
label: key.label,
|
|
103
|
+
disabled: key.disabled,
|
|
104
|
+
editable: key.editable,
|
|
105
|
+
record: cache.records.find((record) => record.id === key.id),
|
|
106
|
+
cooldownSeconds: key.cooldownSeconds,
|
|
107
|
+
lastUsedAt: key.lastUsedAt,
|
|
108
|
+
}))
|
|
109
|
+
: credentials.map((credential) => {
|
|
110
|
+
const id = factoryCredentialId(credential.secret);
|
|
111
|
+
const runtime = runtimeById.get(id);
|
|
112
|
+
return {
|
|
113
|
+
id,
|
|
114
|
+
label: credential.label,
|
|
115
|
+
disabled: false,
|
|
116
|
+
editable: false,
|
|
117
|
+
record: cache.records.find((record) => record.id === id),
|
|
118
|
+
cooldownSeconds: runtime?.cooldownSeconds,
|
|
119
|
+
lastUsedAt: runtime?.lastUsedAt,
|
|
120
|
+
};
|
|
121
|
+
});
|
|
106
122
|
return {
|
|
107
123
|
version: clientVersion,
|
|
108
124
|
modelCount: models.length,
|
|
@@ -110,7 +126,7 @@ export default async function factoryExtension(pi: ExtensionAPI) {
|
|
|
110
126
|
? auth.label || auth.source || "configured"
|
|
111
127
|
: "not configured",
|
|
112
128
|
configured: accounts.length,
|
|
113
|
-
active: accounts.filter((account) => !account.cooldownSeconds).length,
|
|
129
|
+
active: accounts.filter((account) => !account.disabled && !account.cooldownSeconds).length,
|
|
114
130
|
warning: apiKeys.warning,
|
|
115
131
|
accounts,
|
|
116
132
|
};
|
|
@@ -195,10 +211,25 @@ export default async function factoryExtension(pi: ExtensionAPI) {
|
|
|
195
211
|
description: "Open Factory account, rotation, and usage dashboard",
|
|
196
212
|
handler: async (_args, ctx) => {
|
|
197
213
|
const initial = await dashboardSnapshot(ctx);
|
|
198
|
-
await openFactoryDashboard(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
214
|
+
await openFactoryDashboard(
|
|
215
|
+
ctx,
|
|
216
|
+
initial,
|
|
217
|
+
async (force) => {
|
|
218
|
+
await refreshLimits(ctx, force);
|
|
219
|
+
return dashboardSnapshot(ctx);
|
|
220
|
+
},
|
|
221
|
+
async (id, enabled) => {
|
|
222
|
+
if (!setFactoryApiKeyEnabled(id, enabled)) throw new Error("This Factory credential is not editable from the rotating-key file.");
|
|
223
|
+
if (enabled) await refreshLimits(ctx, true);
|
|
224
|
+
return dashboardSnapshot(ctx);
|
|
225
|
+
},
|
|
226
|
+
async (id, label) => {
|
|
227
|
+
const confirmed = await ctx.ui.confirm("Remove Factory account?", `Permanently remove ${label} from the rotating-key file?`);
|
|
228
|
+
if (!confirmed) return dashboardSnapshot(ctx);
|
|
229
|
+
if (!removeFactoryApiKey(id)) throw new Error("This Factory credential is not editable from the rotating-key file.");
|
|
230
|
+
return dashboardSnapshot(ctx);
|
|
231
|
+
},
|
|
232
|
+
);
|
|
202
233
|
},
|
|
203
234
|
});
|
|
204
235
|
}
|
|
@@ -38,8 +38,8 @@ The tool definition and Pi prompt guidance explicitly require the model to:
|
|
|
38
38
|
The runtime reinforces these instructions without assigning the list to another worker:
|
|
39
39
|
|
|
40
40
|
- after two substantive tool calls with no list, the next model context receives a conditional reminder;
|
|
41
|
-
-
|
|
42
|
-
-
|
|
41
|
+
- routine file reads, edits, commands, and tool calls do not trigger bookkeeping updates while the current task remains active;
|
|
42
|
+
- the model updates only for task-level transitions: verified completion and handoff to the next task, genuine blockers or cancellations, user-requested scope changes, and final reconciliation;
|
|
43
43
|
- status-only inspection tools do not count as substantive progress.
|
|
44
44
|
|
|
45
45
|
The list remains a coordination aid, not evidence that implementation or verification succeeded.
|
|
@@ -97,13 +97,8 @@ export default function taskListExtension(pi: ExtensionAPI) {
|
|
|
97
97
|
let state = emptyTaskListState();
|
|
98
98
|
let lastCtx: ExtensionContext | null = null;
|
|
99
99
|
let finishedTimer: ReturnType<typeof setTimeout> | undefined;
|
|
100
|
-
let sequence = 0;
|
|
101
|
-
let lastTaskSequence = 0;
|
|
102
|
-
let lastWorkSequence = 0;
|
|
103
100
|
let workCallsSinceUser = 0;
|
|
104
101
|
let taskCallsSinceUser = 0;
|
|
105
|
-
let nudgeCount = 0;
|
|
106
|
-
let staleAtAgentEnd = false;
|
|
107
102
|
|
|
108
103
|
function cancelFinishedTimer(): void {
|
|
109
104
|
if (!finishedTimer) return;
|
|
@@ -207,9 +202,6 @@ export default function taskListExtension(pi: ExtensionAPI) {
|
|
|
207
202
|
}
|
|
208
203
|
|
|
209
204
|
function reminderText(): string | null {
|
|
210
|
-
if (hasActiveTasks(state) && lastWorkSequence > lastTaskSequence) {
|
|
211
|
-
return "The task list is stale after substantive work. Before more narration, call task_list with the complete updated list: mark finished work completed only if verified, keep current work in_progress, and pair the update with the next action tool when work remains.";
|
|
212
|
-
}
|
|
213
205
|
if (state.tasks.length === 0 && taskCallsSinceUser === 0 && workCallsSinceUser >= 2) {
|
|
214
206
|
return "You have started multi-action work without task_list. If this request requires at least three distinct actions or contains multiple user tasks, create the complete list now and call task_list in the same assistant message as the next action. Do not create a retroactive list if the work is already complete or was genuinely trivial.";
|
|
215
207
|
}
|
|
@@ -220,21 +212,13 @@ export default function taskListExtension(pi: ExtensionAPI) {
|
|
|
220
212
|
if (event.source === "extension") return;
|
|
221
213
|
workCallsSinceUser = 0;
|
|
222
214
|
taskCallsSinceUser = 0;
|
|
223
|
-
lastTaskSequence = 0;
|
|
224
|
-
lastWorkSequence = 0;
|
|
225
|
-
staleAtAgentEnd = false;
|
|
226
|
-
nudgeCount = 0;
|
|
227
215
|
});
|
|
228
216
|
|
|
229
217
|
pi.on("session_start", async (_event, ctx) => {
|
|
230
218
|
lastCtx = ctx;
|
|
231
219
|
state = restoreTaskList(ctx);
|
|
232
|
-
sequence = 0;
|
|
233
|
-
lastTaskSequence = 0;
|
|
234
|
-
lastWorkSequence = 0;
|
|
235
220
|
workCallsSinceUser = 0;
|
|
236
221
|
taskCallsSinceUser = 0;
|
|
237
|
-
nudgeCount = 0;
|
|
238
222
|
updatePresentation(ctx);
|
|
239
223
|
});
|
|
240
224
|
|
|
@@ -245,14 +229,8 @@ export default function taskListExtension(pi: ExtensionAPI) {
|
|
|
245
229
|
|
|
246
230
|
pi.on("tool_execution_start", async (event, ctx) => {
|
|
247
231
|
lastCtx = ctx;
|
|
248
|
-
|
|
249
|
-
if (event.toolName
|
|
250
|
-
lastTaskSequence = sequence;
|
|
251
|
-
taskCallsSinceUser++;
|
|
252
|
-
} else if (!WORK_TOOL_EXCLUSIONS.has(event.toolName)) {
|
|
253
|
-
lastWorkSequence = sequence;
|
|
254
|
-
workCallsSinceUser++;
|
|
255
|
-
}
|
|
232
|
+
if (event.toolName === TASK_LIST_TOOL) taskCallsSinceUser++;
|
|
233
|
+
else if (!WORK_TOOL_EXCLUSIONS.has(event.toolName)) workCallsSinceUser++;
|
|
256
234
|
});
|
|
257
235
|
|
|
258
236
|
pi.on("context", async (event) => {
|
|
@@ -281,23 +259,6 @@ export default function taskListExtension(pi: ExtensionAPI) {
|
|
|
281
259
|
return additions.length > 0 ? { messages: [...event.messages, ...additions] } : undefined;
|
|
282
260
|
});
|
|
283
261
|
|
|
284
|
-
pi.on("agent_end", async () => {
|
|
285
|
-
staleAtAgentEnd = hasActiveTasks(state) && lastWorkSequence > lastTaskSequence;
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
pi.on("agent_settled", async (_event, ctx) => {
|
|
289
|
-
lastCtx = ctx;
|
|
290
|
-
if (!staleAtAgentEnd || nudgeCount >= 1 || ctx.hasPendingMessages()) return;
|
|
291
|
-
staleAtAgentEnd = false;
|
|
292
|
-
nudgeCount++;
|
|
293
|
-
pi.sendMessage({
|
|
294
|
-
customType: "task-list-reminder",
|
|
295
|
-
content: "You stopped with a stale active task list. Update task_list now. If work remains, pair that update with the next concrete action; if work is finished, mark verified items completed and cancelled items with a reason before the final response.",
|
|
296
|
-
display: false,
|
|
297
|
-
details: { revision: state.revision, kind: "settled-stale-list" },
|
|
298
|
-
}, { deliverAs: "followUp", triggerTurn: true });
|
|
299
|
-
});
|
|
300
|
-
|
|
301
262
|
pi.on("session_shutdown", async () => {
|
|
302
263
|
cancelFinishedTimer();
|
|
303
264
|
});
|
|
@@ -398,7 +359,7 @@ Use task_list for work with at least three distinct actions, multiple user-reque
|
|
|
398
359
|
|
|
399
360
|
Start the list before substantive work and call task_list in the SAME assistant message as the first action tool. Never spend a turn only announcing or updating the list when another action can run. Keep stable ids and preserve every user-requested item, exact command, flag, path, and success condition.
|
|
400
361
|
|
|
401
|
-
Update the list
|
|
362
|
+
Update the list only when task-level state changes—not after every file read, edit, command, or tool call. When a task is fully verified, mark it completed, move the next sequential item to in_progress, and issue that next action in the same assistant message. Also update for genuine blockers, cancellations, or user-requested scope changes. Keep one in_progress task for sequential work; use several only when work is genuinely running in parallel.
|
|
402
363
|
|
|
403
364
|
Before the final response, reconcile the whole list with actual results. No item may remain pending or in_progress if the requested work is finished. Do not claim completion from the list itself.`,
|
|
404
365
|
promptSnippet: "Read or replace the current session's complete task list and progress state.",
|
|
@@ -406,7 +367,7 @@ Before the final response, reconcile the whole list with actual results. No item
|
|
|
406
367
|
"Use task_list for requests with at least three distinct actions, multiple requested tasks, or meaningful phases; skip it for direct answers and one- or two-action work.",
|
|
407
368
|
"Create task_list before substantive multi-step work and send the update in the same assistant message as the first real action; never spend a turn on task bookkeeping alone when another action exists.",
|
|
408
369
|
"Every task_list write replaces the entire ordered list. Preserve stable ids, all unfinished and user-requested work, and exact commands, flags, paths, and success conditions.",
|
|
409
|
-
"Update task_list
|
|
370
|
+
"Update task_list only for task-level transitions: verified completion, starting the next task, a genuine blocker or cancellation, or a user-requested scope change. Do not update it after every file read, edit, command, or tool call. Pair transitions with the next action when work remains.",
|
|
410
371
|
"Before a final response, reconcile task_list with observed results and leave no stale pending or in_progress items when the requested work is finished. The list is not proof of completion.",
|
|
411
372
|
],
|
|
412
373
|
parameters: TaskListParams,
|
|
@@ -414,7 +375,6 @@ Before the final response, reconcile the whole list with actual results. No item
|
|
|
414
375
|
lastCtx = ctx;
|
|
415
376
|
if (params.tasks !== undefined) {
|
|
416
377
|
replaceState(buildUpdatedTaskList(state, params), ctx);
|
|
417
|
-
lastTaskSequence = Math.max(lastTaskSequence, sequence);
|
|
418
378
|
}
|
|
419
379
|
const action = params.tasks === undefined ? "read" : "update";
|
|
420
380
|
const details: TaskListDetails = {
|