takomi 2.1.44 → 2.1.45
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/.pi/README.md +4 -3
- package/.pi/extensions/oauth-router/commands.ts +66 -35
- package/.pi/extensions/oauth-router/index.ts +51 -7
- package/.pi/extensions/oauth-router/report-ui.ts +205 -0
- package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +41 -3
- package/.pi/extensions/takomi-context-manager/diagnostics.ts +26 -0
- package/.pi/extensions/takomi-context-manager/index.ts +3 -2
- package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +36 -15
- package/.pi/extensions/takomi-context-manager/policy-tools.ts +93 -42
- package/.pi/extensions/takomi-context-manager/skill-categories.d.ts +11 -0
- package/.pi/extensions/takomi-context-manager/skill-categories.js +212 -0
- package/.pi/extensions/takomi-context-manager/skill-registry.ts +179 -8
- package/.pi/extensions/takomi-context-manager/skill-tools.ts +208 -103
- package/.pi/extensions/takomi-context-manager/tool-renderers.ts +112 -0
- package/.pi/extensions/takomi-context-manager/types.ts +6 -0
- package/.pi/extensions/takomi-runtime/commands.ts +45 -12
- package/.pi/extensions/takomi-runtime/gate-provenance.ts +24 -0
- package/.pi/extensions/takomi-runtime/index.ts +111 -42
- package/.pi/extensions/takomi-runtime/routing-policy.ts +14 -2
- package/.pi/extensions/takomi-runtime/takomi-stats.js +46 -26
- package/.pi/extensions/takomi-runtime/tool-renderers.ts +234 -0
- package/.pi/extensions/takomi-runtime/workflow-catalog.ts +54 -0
- package/.pi/extensions/takomi-subagents/agents.ts +4 -0
- package/.pi/extensions/takomi-subagents/async-lifecycle.ts +401 -0
- package/.pi/extensions/takomi-subagents/detached-results.ts +940 -0
- package/.pi/extensions/takomi-subagents/index.ts +46 -1
- package/.pi/extensions/takomi-subagents/native-render.ts +250 -188
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +55 -46
- package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +11 -5
- package/.pi/extensions/takomi-subagents/result-heartbeat.ts +55 -0
- package/.pi/extensions/takomi-subagents/subagent-ux.ts +162 -0
- package/.pi/extensions/takomi-subagents/tool-runner.ts +158 -28
- package/.pi/takomi/model-routing.md +282 -3
- package/assets/.agent/skills/shared-resend-portfolio/SKILL.md +124 -0
- package/package.json +4 -3
- package/src/pi-takomi-core/types.ts +10 -0
- package/src/pi-takomi-core/workflows.ts +86 -45
- package/src/skills-catalog.js +2 -202
- package/src/skills-installer.js +4 -1
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { loadPiSubagentsInternals, type SubagentState } from "./pi-subagents-internal";
|
|
5
|
+
|
|
6
|
+
const NATIVE_RUNTIME_CLEANUP_KEY = "__piSubagentRuntimeCleanup";
|
|
7
|
+
const TAKOMI_RUNTIME_CLEANUP_KEY = "__takomiPiSubagentRuntimeCleanup";
|
|
8
|
+
const LIFECYCLE_GENERATION_KEY = "__takomiPiSubagentLifecycleGeneration";
|
|
9
|
+
const STARTED_EVENT = "subagent:async-started";
|
|
10
|
+
const COMPLETE_EVENT = "subagent:async-complete";
|
|
11
|
+
const COMPLETION_TTL_MS = 10 * 60 * 1000;
|
|
12
|
+
const COMPLETION_RACE_TTL_MS = 30 * 1000;
|
|
13
|
+
export const TAKOMI_ASYNC_WIDGET_HEARTBEAT_MS = 125;
|
|
14
|
+
|
|
15
|
+
type LifecycleOwnership = "takomi" | "native";
|
|
16
|
+
|
|
17
|
+
export type TakomiAsyncLifecycleSnapshot = {
|
|
18
|
+
state: SubagentState;
|
|
19
|
+
generation: number;
|
|
20
|
+
ownership: LifecycleOwnership;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type RecentCompletion = {
|
|
24
|
+
asyncDir?: string;
|
|
25
|
+
at: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type LifecycleRecord = TakomiAsyncLifecycleSnapshot & {
|
|
29
|
+
internals: any;
|
|
30
|
+
nativeCleanupIdentity?: () => void;
|
|
31
|
+
animationFrame: number;
|
|
32
|
+
animationTimer: ReturnType<typeof setInterval> | null;
|
|
33
|
+
recentCompletions: Map<string, RecentCompletion>;
|
|
34
|
+
activate: (ctx: ExtensionContext) => void;
|
|
35
|
+
prime: () => void;
|
|
36
|
+
cleanup: () => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const lifecycles = new WeakMap<ExtensionAPI, LifecycleRecord>();
|
|
40
|
+
const lifecycleCreations = new WeakMap<ExtensionAPI, Promise<LifecycleRecord>>();
|
|
41
|
+
|
|
42
|
+
function nextGeneration(globalStore: Record<string, unknown>): number {
|
|
43
|
+
const previous = typeof globalStore[LIFECYCLE_GENERATION_KEY] === "number"
|
|
44
|
+
? globalStore[LIFECYCLE_GENERATION_KEY] as number
|
|
45
|
+
: 0;
|
|
46
|
+
const generation = previous + 1;
|
|
47
|
+
globalStore[LIFECYCLE_GENERATION_KEY] = generation;
|
|
48
|
+
return generation;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createState(): SubagentState {
|
|
52
|
+
return {
|
|
53
|
+
// Match native startup isolation: do not claim cwd-only result files until
|
|
54
|
+
// a real Pi session context has activated this lifecycle.
|
|
55
|
+
baseCwd: "",
|
|
56
|
+
currentSessionId: null,
|
|
57
|
+
subagentInProgress: false,
|
|
58
|
+
asyncJobs: new Map(),
|
|
59
|
+
foregroundRuns: new Map(),
|
|
60
|
+
foregroundControls: new Map(),
|
|
61
|
+
lastForegroundControlId: null,
|
|
62
|
+
pendingForegroundControlNotices: new Map(),
|
|
63
|
+
cleanupTimers: new Map(),
|
|
64
|
+
lastUiContext: null,
|
|
65
|
+
poller: null,
|
|
66
|
+
completionSeen: new Map(),
|
|
67
|
+
watcher: null,
|
|
68
|
+
watcherRestartTimer: null,
|
|
69
|
+
resultFileCoalescer: { schedule: () => false, clear: () => {} },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isStaleExtensionContextError(error: unknown): boolean {
|
|
74
|
+
return error instanceof Error && error.message.includes("Extension context no longer active");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderedJobs(record: LifecycleRecord): any[] {
|
|
78
|
+
return Array.from(record.state.asyncJobs.values()).map((job: any) => {
|
|
79
|
+
if (job.status !== "running") return job;
|
|
80
|
+
// pi-subagents@0.31.0 derives the top-level glyph from job timestamps, but
|
|
81
|
+
// derives each Step N/N glyph from that running step's progress snapshot.
|
|
82
|
+
// Give both the same immutable frame-adjusted render copy; lifecycle state
|
|
83
|
+
// stays authoritative and non-running steps retain their static semantics.
|
|
84
|
+
const steps = job.steps?.map((step: any) => step.status === "running"
|
|
85
|
+
? { ...step, durationMs: (step.durationMs ?? 0) + record.animationFrame }
|
|
86
|
+
: step);
|
|
87
|
+
return {
|
|
88
|
+
...job,
|
|
89
|
+
updatedAt: (job.updatedAt ?? 0) + record.animationFrame,
|
|
90
|
+
...(steps ? { steps } : {}),
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeAsyncDir(value: unknown): string | undefined {
|
|
96
|
+
return typeof value === "string" && value
|
|
97
|
+
? path.resolve(value)
|
|
98
|
+
: undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function pruneRecentCompletions(record: LifecycleRecord, now = Date.now()): void {
|
|
102
|
+
for (const [id, completion] of record.recentCompletions) {
|
|
103
|
+
if (now - completion.at > COMPLETION_RACE_TTL_MS) record.recentCompletions.delete(id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isLateStartAfterCompletion(record: LifecycleRecord, info: Record<string, any>, now: number): boolean {
|
|
108
|
+
pruneRecentCompletions(record, now);
|
|
109
|
+
const completion = record.recentCompletions.get(info.id);
|
|
110
|
+
if (!completion) return false;
|
|
111
|
+
const startedAsyncDir = normalizeAsyncDir(info.asyncDir);
|
|
112
|
+
if (!completion.asyncDir || !startedAsyncDir || completion.asyncDir === startedAsyncDir) return true;
|
|
113
|
+
record.recentCompletions.delete(info.id);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stopAnimationHeartbeat(record: LifecycleRecord): void {
|
|
118
|
+
if (!record.animationTimer) return;
|
|
119
|
+
clearInterval(record.animationTimer);
|
|
120
|
+
record.animationTimer = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderJobs(record: LifecycleRecord): boolean {
|
|
124
|
+
const ctx = record.state.lastUiContext;
|
|
125
|
+
if (!ctx) return false;
|
|
126
|
+
try {
|
|
127
|
+
record.internals.renderWidget(ctx, renderedJobs(record));
|
|
128
|
+
ctx.ui.requestRender?.();
|
|
129
|
+
return true;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!isStaleExtensionContextError(error)) throw error;
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function syncAnimationHeartbeat(record: LifecycleRecord): void {
|
|
137
|
+
if (record.state.asyncJobs.size === 0) {
|
|
138
|
+
stopAnimationHeartbeat(record);
|
|
139
|
+
record.animationFrame = 0;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (record.animationTimer || !record.state.lastUiContext?.hasUI) return;
|
|
143
|
+
record.animationTimer = setInterval(() => {
|
|
144
|
+
if (record.state.asyncJobs.size === 0) {
|
|
145
|
+
stopAnimationHeartbeat(record);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
record.animationFrame = (record.animationFrame + 1) % 10;
|
|
149
|
+
if (!renderJobs(record)) stopAnimationHeartbeat(record);
|
|
150
|
+
}, TAKOMI_ASYNC_WIDGET_HEARTBEAT_MS);
|
|
151
|
+
record.animationTimer.unref?.();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function clearWidget(record: LifecycleRecord): void {
|
|
155
|
+
if (record.ownership !== "takomi") return;
|
|
156
|
+
const ctx = record.state.lastUiContext;
|
|
157
|
+
if (!ctx?.hasUI) return;
|
|
158
|
+
try {
|
|
159
|
+
ctx.ui.setWidget(record.internals.WIDGET_KEY, undefined);
|
|
160
|
+
ctx.ui.requestRender?.();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (!isStaleExtensionContextError(error)) throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function clearState(record: LifecycleRecord): void {
|
|
167
|
+
stopAnimationHeartbeat(record);
|
|
168
|
+
record.animationFrame = 0;
|
|
169
|
+
for (const timer of record.state.cleanupTimers.values()) clearTimeout(timer);
|
|
170
|
+
record.state.cleanupTimers.clear();
|
|
171
|
+
for (const timer of record.state.pendingForegroundControlNotices?.values() ?? []) clearTimeout(timer);
|
|
172
|
+
record.state.pendingForegroundControlNotices?.clear();
|
|
173
|
+
if (record.state.poller) clearInterval(record.state.poller);
|
|
174
|
+
record.state.poller = null;
|
|
175
|
+
record.state.asyncJobs.clear();
|
|
176
|
+
record.state.foregroundRuns?.clear();
|
|
177
|
+
record.state.foregroundControls.clear();
|
|
178
|
+
record.state.completionSeen.clear();
|
|
179
|
+
record.state.resultFileCoalescer.clear();
|
|
180
|
+
record.recentCompletions.clear();
|
|
181
|
+
record.state.lastForegroundControlId = null;
|
|
182
|
+
record.state.currentSessionId = null;
|
|
183
|
+
record.state.baseCwd = "";
|
|
184
|
+
record.state.lastUiContext = null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function lifecycleMatchesNativeSlot(record: LifecycleRecord, nativeSlot: unknown): boolean {
|
|
188
|
+
return record.ownership === "takomi"
|
|
189
|
+
? nativeSlot === record.cleanup
|
|
190
|
+
: nativeSlot === record.nativeCleanupIdentity;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function createLifecycleRecord(pi: ExtensionAPI): Promise<LifecycleRecord> {
|
|
194
|
+
const internals = await loadPiSubagentsInternals();
|
|
195
|
+
// Ownership must be sampled after the only asynchronous initialization step;
|
|
196
|
+
// otherwise standalone can take over while internals load and Takomi would
|
|
197
|
+
// incorrectly start a second watcher from the stale pre-await snapshot.
|
|
198
|
+
const globalStore = globalThis as Record<string, unknown>;
|
|
199
|
+
const nativeCleanup = globalStore[NATIVE_RUNTIME_CLEANUP_KEY];
|
|
200
|
+
const ownership: LifecycleOwnership = typeof nativeCleanup === "function" ? "native" : "takomi";
|
|
201
|
+
const state = createState();
|
|
202
|
+
const watcher = ownership === "takomi"
|
|
203
|
+
? internals.createResultWatcher(pi, state, internals.RESULTS_DIR, COMPLETION_TTL_MS)
|
|
204
|
+
: undefined;
|
|
205
|
+
|
|
206
|
+
if (ownership === "takomi") {
|
|
207
|
+
fs.mkdirSync(internals.RESULTS_DIR, { recursive: true });
|
|
208
|
+
fs.mkdirSync(internals.ASYNC_DIR, { recursive: true });
|
|
209
|
+
watcher.startResultWatcher();
|
|
210
|
+
watcher.primeExistingResults();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const record = {
|
|
214
|
+
state,
|
|
215
|
+
internals,
|
|
216
|
+
generation: nextGeneration(globalStore),
|
|
217
|
+
ownership,
|
|
218
|
+
...(ownership === "native" ? { nativeCleanupIdentity: nativeCleanup as () => void } : {}),
|
|
219
|
+
animationFrame: 0,
|
|
220
|
+
animationTimer: null,
|
|
221
|
+
recentCompletions: new Map(),
|
|
222
|
+
activate(ctx: ExtensionContext) {
|
|
223
|
+
state.baseCwd = ctx.cwd;
|
|
224
|
+
state.currentSessionId = internals.resolveCurrentSessionId(ctx.sessionManager);
|
|
225
|
+
state.lastUiContext = ctx;
|
|
226
|
+
},
|
|
227
|
+
prime: () => watcher?.primeExistingResults(),
|
|
228
|
+
cleanup: () => {},
|
|
229
|
+
} as LifecycleRecord;
|
|
230
|
+
|
|
231
|
+
const handleStarted = (payload: unknown) => {
|
|
232
|
+
const info = payload as Record<string, any>;
|
|
233
|
+
if (typeof info.id !== "string" || !info.id) return;
|
|
234
|
+
const now = Date.now();
|
|
235
|
+
if (isLateStartAfterCompletion(record, info, now)) {
|
|
236
|
+
state.subagentInProgress = false;
|
|
237
|
+
syncAnimationHeartbeat(record);
|
|
238
|
+
renderJobs(record);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const agents = Array.isArray(info.agents) && info.agents.length
|
|
242
|
+
? info.agents
|
|
243
|
+
: Array.isArray(info.chain) && info.chain.length ? info.chain : info.agent ? [info.agent] : undefined;
|
|
244
|
+
const parallelGroups = Array.isArray(info.parallelGroups) ? info.parallelGroups : [];
|
|
245
|
+
const firstParallelGroup = parallelGroups.find((group: any) => group?.start === 0);
|
|
246
|
+
const runningStepCount = Math.min(
|
|
247
|
+
agents?.length ?? 0,
|
|
248
|
+
typeof firstParallelGroup?.count === "number" && firstParallelGroup.count > 0 ? firstParallelGroup.count : 1,
|
|
249
|
+
);
|
|
250
|
+
const steps = agents?.map((agent: string, index: number) => ({
|
|
251
|
+
agent,
|
|
252
|
+
index,
|
|
253
|
+
status: index < runningStepCount ? "running" : "pending",
|
|
254
|
+
startedAt: index < runningStepCount ? now : undefined,
|
|
255
|
+
}));
|
|
256
|
+
// The background process has detached. Do not leave the foreground/global
|
|
257
|
+
// single-dispatch guard asserted while its independent widget is active.
|
|
258
|
+
state.subagentInProgress = false;
|
|
259
|
+
state.asyncJobs.set(info.id, {
|
|
260
|
+
asyncId: info.id,
|
|
261
|
+
asyncDir: typeof info.asyncDir === "string" ? info.asyncDir : path.join(internals.ASYNC_DIR, info.id),
|
|
262
|
+
// Native emits async-started only after the background process has spawned.
|
|
263
|
+
status: "running",
|
|
264
|
+
pid: typeof info.pid === "number" ? info.pid : undefined,
|
|
265
|
+
sessionId: typeof info.sessionId === "string" ? info.sessionId : undefined,
|
|
266
|
+
mode: info.mode ?? (info.chain ? "chain" : "single"),
|
|
267
|
+
agents,
|
|
268
|
+
chainStepCount: info.chainStepCount,
|
|
269
|
+
parallelGroups,
|
|
270
|
+
nestedRoute: info.nestedRoute,
|
|
271
|
+
steps,
|
|
272
|
+
stepsTotal: agents?.length,
|
|
273
|
+
runningSteps: runningStepCount,
|
|
274
|
+
completedSteps: 0,
|
|
275
|
+
currentStep: 0,
|
|
276
|
+
hasParallelGroups: parallelGroups.length > 0,
|
|
277
|
+
activeParallelGroup: runningStepCount > 1,
|
|
278
|
+
startedAt: now,
|
|
279
|
+
updatedAt: now,
|
|
280
|
+
controlEventCursor: 0,
|
|
281
|
+
});
|
|
282
|
+
renderJobs(record);
|
|
283
|
+
syncAnimationHeartbeat(record);
|
|
284
|
+
};
|
|
285
|
+
const handleComplete = (payload: unknown) => {
|
|
286
|
+
const result = payload as { id?: unknown; runId?: unknown; asyncDir?: unknown };
|
|
287
|
+
const id = typeof result.id === "string" ? result.id : typeof result.runId === "string" ? result.runId : undefined;
|
|
288
|
+
if (!id) return;
|
|
289
|
+
pruneRecentCompletions(record);
|
|
290
|
+
record.recentCompletions.set(id, {
|
|
291
|
+
asyncDir: normalizeAsyncDir(result.asyncDir),
|
|
292
|
+
at: Date.now(),
|
|
293
|
+
});
|
|
294
|
+
state.asyncJobs.delete(id);
|
|
295
|
+
syncAnimationHeartbeat(record);
|
|
296
|
+
renderJobs(record);
|
|
297
|
+
};
|
|
298
|
+
const eventUnsubscribes = [
|
|
299
|
+
pi.events.on(STARTED_EVENT, handleStarted),
|
|
300
|
+
pi.events.on(COMPLETE_EVENT, handleComplete),
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
let active = true;
|
|
304
|
+
record.cleanup = () => {
|
|
305
|
+
if (!active) return;
|
|
306
|
+
active = false;
|
|
307
|
+
for (const unsubscribe of eventUnsubscribes) {
|
|
308
|
+
try { unsubscribe(); } catch {}
|
|
309
|
+
}
|
|
310
|
+
watcher?.stopResultWatcher();
|
|
311
|
+
clearWidget(record);
|
|
312
|
+
clearState(record);
|
|
313
|
+
if (lifecycles.get(pi) === record) lifecycles.delete(pi);
|
|
314
|
+
if (globalStore[NATIVE_RUNTIME_CLEANUP_KEY] === record.cleanup) delete globalStore[NATIVE_RUNTIME_CLEANUP_KEY];
|
|
315
|
+
if (globalStore[TAKOMI_RUNTIME_CLEANUP_KEY] === record.cleanup) delete globalStore[TAKOMI_RUNTIME_CLEANUP_KEY];
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
lifecycles.set(pi, record);
|
|
319
|
+
if (ownership === "takomi") globalStore[NATIVE_RUNTIME_CLEANUP_KEY] = record.cleanup;
|
|
320
|
+
globalStore[TAKOMI_RUNTIME_CLEANUP_KEY] = record.cleanup;
|
|
321
|
+
return record;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function getOrCreateLifecycleRecord(pi: ExtensionAPI): Promise<LifecycleRecord> {
|
|
325
|
+
const existing = lifecycleCreations.get(pi);
|
|
326
|
+
if (existing) return existing;
|
|
327
|
+
const creation = createLifecycleRecord(pi);
|
|
328
|
+
lifecycleCreations.set(pi, creation);
|
|
329
|
+
void creation.then(
|
|
330
|
+
() => { if (lifecycleCreations.get(pi) === creation) lifecycleCreations.delete(pi); },
|
|
331
|
+
() => { if (lifecycleCreations.get(pi) === creation) lifecycleCreations.delete(pi); },
|
|
332
|
+
);
|
|
333
|
+
return creation;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Ensure Takomi always has its own executor state. When standalone pi-subagents
|
|
338
|
+
* owns the native watcher, this record only subscribes to events and tracks
|
|
339
|
+
* state. Rendering reuses the native WIDGET_KEY, so post-spawn running state
|
|
340
|
+
* replaces (rather than duplicates) the standalone queued surface.
|
|
341
|
+
*/
|
|
342
|
+
export async function ensureTakomiAsyncLifecycle(
|
|
343
|
+
pi: ExtensionAPI,
|
|
344
|
+
ctx?: ExtensionContext,
|
|
345
|
+
): Promise<TakomiAsyncLifecycleSnapshot> {
|
|
346
|
+
const globalStore = globalThis as Record<string, unknown>;
|
|
347
|
+
for (;;) {
|
|
348
|
+
let record = lifecycles.get(pi);
|
|
349
|
+
if (record && !lifecycleMatchesNativeSlot(record, globalStore[NATIVE_RUNTIME_CLEANUP_KEY])) {
|
|
350
|
+
record.cleanup();
|
|
351
|
+
record = undefined;
|
|
352
|
+
}
|
|
353
|
+
if (!record) record = await getOrCreateLifecycleRecord(pi);
|
|
354
|
+
// Revalidate after awaiting shared initialization so a native takeover queued
|
|
355
|
+
// in the same turn cannot expose a just-disposed state to an executor.
|
|
356
|
+
if (!lifecycleMatchesNativeSlot(record, globalStore[NATIVE_RUNTIME_CLEANUP_KEY])) {
|
|
357
|
+
record.cleanup();
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (ctx) record.activate(ctx);
|
|
361
|
+
return { state: record.state, generation: record.generation, ownership: record.ownership };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export async function initializeTakomiAsyncLifecycle(pi: ExtensionAPI): Promise<() => void> {
|
|
366
|
+
const globalStore = globalThis as Record<string, unknown>;
|
|
367
|
+
const staleTakomiCleanup = globalStore[TAKOMI_RUNTIME_CLEANUP_KEY];
|
|
368
|
+
if (typeof staleTakomiCleanup === "function") {
|
|
369
|
+
try { staleTakomiCleanup(); } catch {}
|
|
370
|
+
}
|
|
371
|
+
await ensureTakomiAsyncLifecycle(pi);
|
|
372
|
+
// Resolve the current record at cleanup time: native takeover can replace the
|
|
373
|
+
// record after registration, and shutdown must not leave that replacement live.
|
|
374
|
+
return () => cleanupTakomiAsyncLifecycle(pi);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function cleanupTakomiAsyncLifecycle(pi: ExtensionAPI): void {
|
|
378
|
+
lifecycles.get(pi)?.cleanup();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export async function resetTakomiAsyncLifecycle(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
382
|
+
const snapshot = await ensureTakomiAsyncLifecycle(pi, ctx);
|
|
383
|
+
const record = lifecycles.get(pi);
|
|
384
|
+
if (!record || record.state !== snapshot.state) return;
|
|
385
|
+
record.state.resultFileCoalescer.clear();
|
|
386
|
+
record.recentCompletions.clear();
|
|
387
|
+
record.state.asyncJobs.clear();
|
|
388
|
+
syncAnimationHeartbeat(record);
|
|
389
|
+
clearWidget(record);
|
|
390
|
+
record.prime();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function getTakomiAsyncLifecycleSnapshot(pi: ExtensionAPI): TakomiAsyncLifecycleSnapshot | undefined {
|
|
394
|
+
const record = lifecycles.get(pi);
|
|
395
|
+
if (!record) return undefined;
|
|
396
|
+
return { state: record.state, generation: record.generation, ownership: record.ownership };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function getTakomiAsyncLifecycleState(pi: ExtensionAPI): SubagentState | undefined {
|
|
400
|
+
return lifecycles.get(pi)?.state;
|
|
401
|
+
}
|