pi-agy-acp 0.1.0 → 0.1.1

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.
@@ -70,6 +70,30 @@ To inspect the package contents without publishing:
70
70
  npm pack --dry-run
71
71
  ```
72
72
 
73
+ ## CI and releases
74
+
75
+ `ci.yml` checks pull requests and pushes to `main`. `release.yml` runs only for
76
+ version-tag pushes or manual runs on an existing tag, so ordinary commits do not
77
+ run a second test suite or trigger publication. Both use Node.js 24. There is no
78
+ committed lockfile, so dependency installation uses `npm install`.
79
+
80
+ To release, bump `package.json`, commit the change, and push its matching
81
+ `vX.Y.Z` tag. The workflow accepts stable versions only; publish one version at
82
+ a time and never move a published tag. It validates the tag and version, then
83
+ checks, tests, and packs before publishing. Only a registry 404 means the version
84
+ is absent; network, authentication, and server errors stop the job.
85
+
86
+ Configure npm [Trusted Publishing](https://docs.npmjs.com/trusted-publishers/)
87
+ for owner `fdkevin0`, repository `pi-agy-acp`, and workflow filename `release.yml`.
88
+ The workflow grants OIDC permission to the publish job and uses npm >= 11.5.1.
89
+ An optional `NPM_TOKEN` repository secret supports token authentication through
90
+ `NODE_AUTH_TOKEN`; use a granular write token with the required 2FA bypass.
91
+
92
+ If npm publication succeeds but GitHub Release creation fails, rerun that tagged
93
+ workflow, or use `gh workflow run release.yml --ref vX.Y.Z`. Branch-based manual
94
+ runs are rejected. Existing npm versions are not republished, and GitHub Releases
95
+ are created only for existing tags with `gh release create --verify-tag`.
96
+
73
97
  ## Documentation conventions
74
98
 
75
99
  Keep all project documentation in English. Use progressive disclosure:
package/docs/reference.md CHANGED
@@ -98,6 +98,14 @@ PI_AGY_COMMAND=/absolute/path/to/agy pi -e ./index.ts
98
98
  The dedicated directory isolates the bridge agent from your normal agy tools and
99
99
  plugins. It contains agy-managed session data, logs, and the extension's model cache.
100
100
 
101
+ Closed temporary ACP sessions have their database files and brain directories
102
+ removed automatically. Before starting ACP, the extension also removes leftover
103
+ session files, summary indexes, and numeric `acp.log.<pid>` backups. It retains
104
+ the current `acp.log`, model cache, and authentication files; Pi owns the history
105
+ used to rebuild sessions. Cleanup is best-effort if files are locked. Use a
106
+ different `PI_AGY_HOME` for each concurrently running Pi instance, since startup
107
+ cleanup assumes exclusive use of the directory.
108
+
101
109
  With file-backed authentication, the extension copies the existing OAuth token
102
110
  from your normal agy directory and sets its permissions to `0600`. Do not commit
103
111
  or share this directory. Authentication and token refresh remain agy's responsibility.
@@ -106,7 +114,9 @@ or share this directory. Authentication and token refresh remain agy's responsib
106
114
 
107
115
  Loading the extension reads the cached model catalog, or falls back to five model
108
116
  families, without starting agy. Pi's model refresh queries ACP for the account's
109
- catalog. The first request also discovers models and validates the selection.
117
+ catalog once per runtime. Concurrent refreshes share one discovery session;
118
+ after a refresh or normal session observes the live catalog, later refreshes
119
+ reuse it. The first request also discovers models and validates the selection.
110
120
  Cache-only refreshes and refreshes already cancelled before startup do not launch
111
121
  ACP. Server-advertised models are not hidden, and live catalog changes update
112
122
  Pi's model selector. The selector exposes only the ACP reasoning levels offered
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agy-acp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Google Antigravity ACP provider extension for Pi Coding Agent",
package/src/preflight.ts CHANGED
@@ -16,6 +16,9 @@ import {
16
16
  writeFileSync,
17
17
  readFileSync,
18
18
  statSync,
19
+ lstatSync,
20
+ readdirSync,
21
+ rmSync,
19
22
  } from 'node:fs'
20
23
  import { homedir } from 'node:os'
21
24
  import { delimiter, isAbsolute, join } from 'node:path'
@@ -208,6 +211,46 @@ export function syncOAuthToken(
208
211
  }
209
212
  }
210
213
 
214
+ /** Remove only one closed ACP session's files; session ids come from the agent. */
215
+ export function removeSessionArtifacts(home: string, sessionId: string): void {
216
+ if (
217
+ typeof sessionId !== 'string' ||
218
+ sessionId.length === 0 ||
219
+ /[^A-Za-z0-9._-]/.test(sessionId) ||
220
+ sessionId === '.' ||
221
+ sessionId.includes('..')
222
+ )
223
+ return
224
+ const appData = join(home, 'agy')
225
+ if (!isRealDirectory(appData)) return
226
+ removeArtifacts(join(appData, 'conversations'), [
227
+ `${sessionId}.db`,
228
+ `${sessionId}.db-wal`,
229
+ `${sessionId}.db-shm`,
230
+ ])
231
+ removeArtifacts(join(appData, 'brain'), [sessionId])
232
+ }
233
+
234
+ function isRealDirectory(path: string): boolean {
235
+ try {
236
+ return lstatSync(path).isDirectory()
237
+ } catch {
238
+ return false
239
+ }
240
+ }
241
+
242
+ function removeArtifacts(directory: string, names: readonly string[]): void {
243
+ // Never follow a replaced parent directory into data outside the isolated home.
244
+ if (!isRealDirectory(directory)) return
245
+ for (const name of names) {
246
+ try {
247
+ rmSync(join(directory, name), { recursive: true, force: true })
248
+ } catch {
249
+ // Locked or unreadable leftovers can be retried on the next startup.
250
+ }
251
+ }
252
+ }
253
+
211
254
  /**
212
255
  * Write the isolated Antigravity home: bridge agent and ACP settings.
213
256
  * @param home - absolute home directory used as `--gemini-dir`.
@@ -240,6 +283,24 @@ export function prepareHome(
240
283
  )
241
284
  const appData = join(home, 'agy')
242
285
  mkdirSync(appData, { recursive: true })
286
+ // Pi owns conversation history; no ACP session is restored across processes.
287
+ // shortcut: cleanup requires an exclusive home; add locking before supporting shared homes.
288
+ removeArtifacts(appData, [
289
+ 'conversations',
290
+ 'brain',
291
+ 'conversation_summaries.db',
292
+ 'conversation_summaries.db-wal',
293
+ 'conversation_summaries.db-shm',
294
+ 'jetbox_summaries_proto.pb',
295
+ ])
296
+ try {
297
+ removeArtifacts(
298
+ home,
299
+ readdirSync(home).filter((name) => /^acp\.log\.\d+$/.test(name)),
300
+ )
301
+ } catch {
302
+ // Log cleanup is best-effort when the home cannot be listed.
303
+ }
243
304
  syncOAuthToken(home, userHome)
244
305
  const settingsPath = join(appData, 'settings.json')
245
306
  const settings = readJson(settingsPath)
package/src/runtime.ts CHANGED
@@ -67,6 +67,7 @@ import {
67
67
  GPT_OSS_CONTEXT_WINDOW,
68
68
  DEFAULT_CONTEXT_WINDOW,
69
69
  prepareHome,
70
+ removeSessionArtifacts,
70
71
  readVersion,
71
72
  resolveAgyBinary,
72
73
  syncOAuthToken,
@@ -184,6 +185,7 @@ export class AntigravityRuntime {
184
185
  private state: RuntimeStatus['state'] = 'idle'
185
186
  private lastError: string | undefined
186
187
  private tierDiscovery: Promise<void> | undefined
188
+ private warmupPromise: Promise<void> | undefined
187
189
  private readonly conversations = new Map<string, Conversation>()
188
190
  private readonly tenants = new Map<string, Conversation>()
189
191
  private readonly mutex = new KeyedMutex()
@@ -344,24 +346,39 @@ export class AntigravityRuntime {
344
346
  * @returns resolves when discovery finished.
345
347
  */
346
348
  async warmup(): Promise<void> {
347
- const proc = await this.ensureReady()
348
- const result = await proc.newSession(
349
- {
350
- cwd: this.preflight?.workspace ?? this.home,
351
- mcpServers: [],
352
- },
353
- { onUpdate: () => undefined },
354
- 120_000,
355
- )
356
- if (
357
- this.catalog.observe(
358
- result.configOptions,
359
- result.models?.availableModels,
360
- result._meta?.antigravity?.tier,
349
+ if (this.warmupPromise !== undefined) return this.warmupPromise
350
+ if (this.catalog.observed) return
351
+ this.warmupPromise = (async () => {
352
+ const proc = await this.ensureReady()
353
+ if (this.catalog.observed) return
354
+ const result = await proc.newSession(
355
+ {
356
+ cwd: this.preflight?.workspace ?? this.home,
357
+ mcpServers: [],
358
+ },
359
+ { onUpdate: () => undefined },
360
+ 120_000,
361
361
  )
362
- )
363
- this.host.onModelsChanged()
364
- await proc.closeSession(result.sessionId)
362
+ try {
363
+ if (
364
+ this.catalog.observe(
365
+ result.configOptions,
366
+ result.models?.availableModels,
367
+ result._meta?.antigravity?.tier,
368
+ )
369
+ )
370
+ this.host.onModelsChanged()
371
+ } finally {
372
+ try {
373
+ await proc.closeSession(result.sessionId)
374
+ } finally {
375
+ removeSessionArtifacts(this.home, result.sessionId)
376
+ }
377
+ }
378
+ })().finally(() => {
379
+ this.warmupPromise = undefined
380
+ })
381
+ return this.warmupPromise
365
382
  }
366
383
 
367
384
  /** Stop every session, the process, and the bridge. */
@@ -593,7 +610,10 @@ export class AntigravityRuntime {
593
610
  }
594
611
 
595
612
  private runPreflight(): Promise<PreflightResult> {
596
- if (this.preflight !== undefined) return Promise.resolve(this.preflight)
613
+ if (this.preflight !== undefined) {
614
+ prepareHome(this.home)
615
+ return Promise.resolve(this.preflight)
616
+ }
597
617
  this.preflightPromise ??= (async () => {
598
618
  const config = this.host.config
599
619
  const binary = resolveAgyBinary(config.command)
@@ -966,7 +986,10 @@ export class AntigravityRuntime {
966
986
 
967
987
  private async resetSession(conv: Conversation): Promise<void> {
968
988
  if (conv.sessionId !== undefined && conv.process !== undefined) {
969
- void conv.process.closeSession(conv.sessionId)
989
+ const sessionId = conv.sessionId
990
+ void conv.process
991
+ .closeSession(sessionId)
992
+ .finally(() => removeSessionArtifacts(this.home, sessionId))
970
993
  }
971
994
  this.releaseTenant(conv)
972
995
  conv.sessionId = undefined