prjct-cli 1.18.0 → 1.20.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 +122 -0
- package/core/__tests__/storage/archive-storage.test.ts +455 -0
- package/core/__tests__/utils/retry.test.ts +381 -0
- package/core/agentic/tool-registry.ts +40 -12
- package/core/schemas/ideas.ts +1 -1
- package/core/services/agent-generator.ts +35 -8
- package/core/services/agent-service.ts +17 -12
- package/core/services/memory-service.ts +42 -0
- package/core/services/sync-service.ts +51 -0
- package/core/storage/archive-storage.ts +205 -0
- package/core/storage/database.ts +24 -0
- package/core/storage/ideas-storage.ts +54 -2
- package/core/storage/index.ts +2 -0
- package/core/storage/queue-storage.ts +43 -1
- package/core/storage/shipped-storage.ts +45 -1
- package/core/storage/state-storage.ts +16 -2
- package/core/types/storage.ts +1 -1
- package/core/utils/retry.ts +318 -0
- package/dist/bin/prjct.mjs +612 -21
- package/package.json +1 -1
package/dist/bin/prjct.mjs
CHANGED
|
@@ -4277,6 +4277,30 @@ var init_database = __esm({
|
|
|
4277
4277
|
);
|
|
4278
4278
|
`);
|
|
4279
4279
|
}, "up")
|
|
4280
|
+
},
|
|
4281
|
+
{
|
|
4282
|
+
version: 2,
|
|
4283
|
+
name: "archives-table",
|
|
4284
|
+
up: /* @__PURE__ */ __name((db) => {
|
|
4285
|
+
db.run(`
|
|
4286
|
+
-- =======================================================================
|
|
4287
|
+
-- Archives: Stale data moved out of active storage (PRJ-267)
|
|
4288
|
+
-- =======================================================================
|
|
4289
|
+
CREATE TABLE archives (
|
|
4290
|
+
id TEXT PRIMARY KEY,
|
|
4291
|
+
entity_type TEXT NOT NULL,
|
|
4292
|
+
entity_id TEXT NOT NULL,
|
|
4293
|
+
entity_data TEXT NOT NULL,
|
|
4294
|
+
summary TEXT,
|
|
4295
|
+
archived_at TEXT NOT NULL,
|
|
4296
|
+
reason TEXT NOT NULL
|
|
4297
|
+
);
|
|
4298
|
+
|
|
4299
|
+
CREATE INDEX idx_archives_entity_type ON archives(entity_type);
|
|
4300
|
+
CREATE INDEX idx_archives_archived_at ON archives(archived_at);
|
|
4301
|
+
CREATE INDEX idx_archives_entity_id ON archives(entity_id);
|
|
4302
|
+
`);
|
|
4303
|
+
}, "up")
|
|
4280
4304
|
}
|
|
4281
4305
|
];
|
|
4282
4306
|
PrjctDatabase = class {
|
|
@@ -9716,7 +9740,7 @@ var init_ideas = __esm({
|
|
|
9716
9740
|
"core/schemas/ideas.ts"() {
|
|
9717
9741
|
"use strict";
|
|
9718
9742
|
IdeaPrioritySchema = z6.enum(["low", "medium", "high"]);
|
|
9719
|
-
IdeaStatusSchema = z6.enum(["pending", "converted", "completed", "archived"]);
|
|
9743
|
+
IdeaStatusSchema = z6.enum(["pending", "converted", "completed", "archived", "dormant"]);
|
|
9720
9744
|
ImpactLevelSchema = z6.enum(["high", "medium", "low"]);
|
|
9721
9745
|
ImpactEffortSchema = z6.object({
|
|
9722
9746
|
impact: ImpactLevelSchema,
|
|
@@ -12726,6 +12750,147 @@ var init_analysis_storage = __esm({
|
|
|
12726
12750
|
}
|
|
12727
12751
|
});
|
|
12728
12752
|
|
|
12753
|
+
// core/storage/archive-storage.ts
|
|
12754
|
+
var ARCHIVE_POLICIES, ArchiveStorage, archiveStorage;
|
|
12755
|
+
var init_archive_storage = __esm({
|
|
12756
|
+
"core/storage/archive-storage.ts"() {
|
|
12757
|
+
"use strict";
|
|
12758
|
+
init_schemas2();
|
|
12759
|
+
init_date_helper();
|
|
12760
|
+
init_database();
|
|
12761
|
+
ARCHIVE_POLICIES = {
|
|
12762
|
+
SHIPPED_RETENTION_DAYS: 90,
|
|
12763
|
+
IDEA_DORMANT_DAYS: 180,
|
|
12764
|
+
QUEUE_COMPLETED_DAYS: 7,
|
|
12765
|
+
PAUSED_TASK_DAYS: 30,
|
|
12766
|
+
MEMORY_MAX_ENTRIES: 500
|
|
12767
|
+
};
|
|
12768
|
+
ArchiveStorage = class {
|
|
12769
|
+
static {
|
|
12770
|
+
__name(this, "ArchiveStorage");
|
|
12771
|
+
}
|
|
12772
|
+
/**
|
|
12773
|
+
* Archive a single item
|
|
12774
|
+
*/
|
|
12775
|
+
archive(projectId, item) {
|
|
12776
|
+
const id = generateUUID();
|
|
12777
|
+
const now = getTimestamp();
|
|
12778
|
+
prjctDb.run(
|
|
12779
|
+
projectId,
|
|
12780
|
+
"INSERT INTO archives (id, entity_type, entity_id, entity_data, summary, archived_at, reason) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
12781
|
+
id,
|
|
12782
|
+
item.entityType,
|
|
12783
|
+
item.entityId,
|
|
12784
|
+
JSON.stringify(item.entityData),
|
|
12785
|
+
item.summary ?? null,
|
|
12786
|
+
now,
|
|
12787
|
+
item.reason
|
|
12788
|
+
);
|
|
12789
|
+
return id;
|
|
12790
|
+
}
|
|
12791
|
+
/**
|
|
12792
|
+
* Archive multiple items in a transaction
|
|
12793
|
+
*/
|
|
12794
|
+
archiveMany(projectId, items) {
|
|
12795
|
+
if (items.length === 0) return 0;
|
|
12796
|
+
const now = getTimestamp();
|
|
12797
|
+
prjctDb.transaction(projectId, (db) => {
|
|
12798
|
+
const stmt = db.prepare(
|
|
12799
|
+
"INSERT INTO archives (id, entity_type, entity_id, entity_data, summary, archived_at, reason) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
12800
|
+
);
|
|
12801
|
+
for (const item of items) {
|
|
12802
|
+
stmt.run(
|
|
12803
|
+
generateUUID(),
|
|
12804
|
+
item.entityType,
|
|
12805
|
+
item.entityId,
|
|
12806
|
+
JSON.stringify(item.entityData),
|
|
12807
|
+
item.summary ?? null,
|
|
12808
|
+
now,
|
|
12809
|
+
item.reason
|
|
12810
|
+
);
|
|
12811
|
+
}
|
|
12812
|
+
});
|
|
12813
|
+
return items.length;
|
|
12814
|
+
}
|
|
12815
|
+
/**
|
|
12816
|
+
* Query archived items by type
|
|
12817
|
+
*/
|
|
12818
|
+
getArchived(projectId, entityType, limit = 50) {
|
|
12819
|
+
if (entityType) {
|
|
12820
|
+
return prjctDb.query(
|
|
12821
|
+
projectId,
|
|
12822
|
+
"SELECT * FROM archives WHERE entity_type = ? ORDER BY archived_at DESC LIMIT ?",
|
|
12823
|
+
entityType,
|
|
12824
|
+
limit
|
|
12825
|
+
);
|
|
12826
|
+
}
|
|
12827
|
+
return prjctDb.query(
|
|
12828
|
+
projectId,
|
|
12829
|
+
"SELECT * FROM archives ORDER BY archived_at DESC LIMIT ?",
|
|
12830
|
+
limit
|
|
12831
|
+
);
|
|
12832
|
+
}
|
|
12833
|
+
/**
|
|
12834
|
+
* Get count of archived items by type
|
|
12835
|
+
*/
|
|
12836
|
+
getStats(projectId) {
|
|
12837
|
+
const rows = prjctDb.query(
|
|
12838
|
+
projectId,
|
|
12839
|
+
"SELECT entity_type, COUNT(*) as count FROM archives GROUP BY entity_type"
|
|
12840
|
+
);
|
|
12841
|
+
const stats = {
|
|
12842
|
+
shipped: 0,
|
|
12843
|
+
idea: 0,
|
|
12844
|
+
queue_task: 0,
|
|
12845
|
+
paused_task: 0,
|
|
12846
|
+
memory_entry: 0,
|
|
12847
|
+
total: 0
|
|
12848
|
+
};
|
|
12849
|
+
for (const row of rows) {
|
|
12850
|
+
const type = row.entity_type;
|
|
12851
|
+
if (type in stats) {
|
|
12852
|
+
stats[type] = row.count;
|
|
12853
|
+
}
|
|
12854
|
+
stats.total += row.count;
|
|
12855
|
+
}
|
|
12856
|
+
return stats;
|
|
12857
|
+
}
|
|
12858
|
+
/**
|
|
12859
|
+
* Restore an item from archive (removes from archive table)
|
|
12860
|
+
* Returns the entity data for the caller to re-insert into active storage.
|
|
12861
|
+
*/
|
|
12862
|
+
restore(projectId, archiveId) {
|
|
12863
|
+
const record = prjctDb.get(
|
|
12864
|
+
projectId,
|
|
12865
|
+
"SELECT * FROM archives WHERE id = ?",
|
|
12866
|
+
archiveId
|
|
12867
|
+
);
|
|
12868
|
+
if (!record) return null;
|
|
12869
|
+
prjctDb.run(projectId, "DELETE FROM archives WHERE id = ?", archiveId);
|
|
12870
|
+
return JSON.parse(record.entity_data);
|
|
12871
|
+
}
|
|
12872
|
+
/**
|
|
12873
|
+
* Permanently delete archives older than N days
|
|
12874
|
+
*/
|
|
12875
|
+
pruneOldArchives(projectId, olderThanDays) {
|
|
12876
|
+
const threshold = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1e3).toISOString();
|
|
12877
|
+
const before = this.getTotalCount(projectId);
|
|
12878
|
+
prjctDb.run(projectId, "DELETE FROM archives WHERE archived_at < ?", threshold);
|
|
12879
|
+
const after = this.getTotalCount(projectId);
|
|
12880
|
+
return before - after;
|
|
12881
|
+
}
|
|
12882
|
+
/**
|
|
12883
|
+
* Get total count of archived items
|
|
12884
|
+
*/
|
|
12885
|
+
getTotalCount(projectId) {
|
|
12886
|
+
const row = prjctDb.get(projectId, "SELECT COUNT(*) as count FROM archives");
|
|
12887
|
+
return row?.count ?? 0;
|
|
12888
|
+
}
|
|
12889
|
+
};
|
|
12890
|
+
archiveStorage = new ArchiveStorage();
|
|
12891
|
+
}
|
|
12892
|
+
});
|
|
12893
|
+
|
|
12729
12894
|
// core/storage/ideas-storage.ts
|
|
12730
12895
|
var IdeasStorage, ideasStorage;
|
|
12731
12896
|
var init_ideas_storage = __esm({
|
|
@@ -12734,6 +12899,7 @@ var init_ideas_storage = __esm({
|
|
|
12734
12899
|
init_schemas2();
|
|
12735
12900
|
init_ideas();
|
|
12736
12901
|
init_date_helper();
|
|
12902
|
+
init_archive_storage();
|
|
12737
12903
|
init_storage_manager();
|
|
12738
12904
|
IdeasStorage = class extends StorageManager {
|
|
12739
12905
|
static {
|
|
@@ -12762,6 +12928,7 @@ var init_ideas_storage = __esm({
|
|
|
12762
12928
|
const pending = data.ideas.filter((i) => i.status === "pending");
|
|
12763
12929
|
const converted = data.ideas.filter((i) => i.status === "converted");
|
|
12764
12930
|
const archived = data.ideas.filter((i) => i.status === "archived");
|
|
12931
|
+
const dormant = data.ideas.filter((i) => i.status === "dormant");
|
|
12765
12932
|
lines.push("## Brain Dump");
|
|
12766
12933
|
if (pending.length > 0) {
|
|
12767
12934
|
pending.forEach((idea) => {
|
|
@@ -12791,6 +12958,10 @@ var init_ideas_storage = __esm({
|
|
|
12791
12958
|
});
|
|
12792
12959
|
lines.push("");
|
|
12793
12960
|
}
|
|
12961
|
+
if (dormant.length > 0) {
|
|
12962
|
+
lines.push(`_${dormant.length} dormant idea(s) excluded from context_`);
|
|
12963
|
+
lines.push("");
|
|
12964
|
+
}
|
|
12794
12965
|
return lines.join("\n");
|
|
12795
12966
|
}
|
|
12796
12967
|
// =========== Domain Methods ===========
|
|
@@ -12926,6 +13097,40 @@ var init_ideas_storage = __esm({
|
|
|
12926
13097
|
}));
|
|
12927
13098
|
return { removed };
|
|
12928
13099
|
}
|
|
13100
|
+
/**
|
|
13101
|
+
* Mark pending ideas older than retention period as dormant (PRJ-267).
|
|
13102
|
+
* Dormant ideas are excluded from LLM context but remain queryable.
|
|
13103
|
+
* Returns count of newly dormant ideas.
|
|
13104
|
+
*/
|
|
13105
|
+
async markDormantIdeas(projectId) {
|
|
13106
|
+
const data = await this.read(projectId);
|
|
13107
|
+
const threshold = getDaysAgo(ARCHIVE_POLICIES.IDEA_DORMANT_DAYS);
|
|
13108
|
+
const stalePending = data.ideas.filter(
|
|
13109
|
+
(i) => i.status === "pending" && new Date(i.addedAt) < threshold
|
|
13110
|
+
);
|
|
13111
|
+
if (stalePending.length === 0) return 0;
|
|
13112
|
+
archiveStorage.archiveMany(
|
|
13113
|
+
projectId,
|
|
13114
|
+
stalePending.map((idea) => ({
|
|
13115
|
+
entityType: "idea",
|
|
13116
|
+
entityId: idea.id,
|
|
13117
|
+
entityData: idea,
|
|
13118
|
+
summary: idea.text,
|
|
13119
|
+
reason: "dormant"
|
|
13120
|
+
}))
|
|
13121
|
+
);
|
|
13122
|
+
const staleIds = new Set(stalePending.map((i) => i.id));
|
|
13123
|
+
await this.update(projectId, (d) => ({
|
|
13124
|
+
ideas: d.ideas.map(
|
|
13125
|
+
(i) => staleIds.has(i.id) ? { ...i, status: "dormant" } : i
|
|
13126
|
+
),
|
|
13127
|
+
lastUpdated: getTimestamp()
|
|
13128
|
+
}));
|
|
13129
|
+
await this.publishEvent(projectId, "ideas.dormant", {
|
|
13130
|
+
count: stalePending.length
|
|
13131
|
+
});
|
|
13132
|
+
return stalePending.length;
|
|
13133
|
+
}
|
|
12929
13134
|
};
|
|
12930
13135
|
ideasStorage = new IdeasStorage();
|
|
12931
13136
|
}
|
|
@@ -13764,6 +13969,7 @@ var init_queue_storage = __esm({
|
|
|
13764
13969
|
init_schemas2();
|
|
13765
13970
|
init_state();
|
|
13766
13971
|
init_date_helper();
|
|
13972
|
+
init_archive_storage();
|
|
13767
13973
|
init_storage_manager();
|
|
13768
13974
|
QueueStorage = class extends StorageManager {
|
|
13769
13975
|
static {
|
|
@@ -13969,6 +14175,38 @@ var init_queue_storage = __esm({
|
|
|
13969
14175
|
}));
|
|
13970
14176
|
return completedCount;
|
|
13971
14177
|
}
|
|
14178
|
+
/**
|
|
14179
|
+
* Remove completed tasks older than retention period (PRJ-267).
|
|
14180
|
+
* Archives them to SQLite before removal.
|
|
14181
|
+
* Returns count of removed tasks.
|
|
14182
|
+
*/
|
|
14183
|
+
async removeStaleCompleted(projectId) {
|
|
14184
|
+
const queue = await this.read(projectId);
|
|
14185
|
+
const threshold = getDaysAgo(ARCHIVE_POLICIES.QUEUE_COMPLETED_DAYS);
|
|
14186
|
+
const stale = queue.tasks.filter(
|
|
14187
|
+
(t) => t.completed && t.completedAt && new Date(t.completedAt) < threshold
|
|
14188
|
+
);
|
|
14189
|
+
if (stale.length === 0) return 0;
|
|
14190
|
+
archiveStorage.archiveMany(
|
|
14191
|
+
projectId,
|
|
14192
|
+
stale.map((t) => ({
|
|
14193
|
+
entityType: "queue_task",
|
|
14194
|
+
entityId: t.id,
|
|
14195
|
+
entityData: t,
|
|
14196
|
+
summary: t.description,
|
|
14197
|
+
reason: "age"
|
|
14198
|
+
}))
|
|
14199
|
+
);
|
|
14200
|
+
const staleIds = new Set(stale.map((t) => t.id));
|
|
14201
|
+
await this.update(projectId, (q) => ({
|
|
14202
|
+
tasks: q.tasks.filter((t) => !staleIds.has(t.id)),
|
|
14203
|
+
lastUpdated: getTimestamp()
|
|
14204
|
+
}));
|
|
14205
|
+
await this.publishEvent(projectId, "queue.stale_removed", {
|
|
14206
|
+
count: stale.length
|
|
14207
|
+
});
|
|
14208
|
+
return stale.length;
|
|
14209
|
+
}
|
|
13972
14210
|
/**
|
|
13973
14211
|
* Sort tasks by priority and section
|
|
13974
14212
|
*/
|
|
@@ -14005,6 +14243,7 @@ var init_shipped_storage = __esm({
|
|
|
14005
14243
|
init_schemas2();
|
|
14006
14244
|
init_shipped();
|
|
14007
14245
|
init_date_helper();
|
|
14246
|
+
init_archive_storage();
|
|
14008
14247
|
init_storage_manager();
|
|
14009
14248
|
ShippedStorage = class extends StorageManager {
|
|
14010
14249
|
static {
|
|
@@ -14154,6 +14393,39 @@ var init_shipped_storage = __esm({
|
|
|
14154
14393
|
period
|
|
14155
14394
|
};
|
|
14156
14395
|
}
|
|
14396
|
+
/**
|
|
14397
|
+
* Archive shipped features older than retention period (PRJ-267).
|
|
14398
|
+
* Moves old items to archive table, keeps 1-line summary in active storage.
|
|
14399
|
+
* Returns count of archived items.
|
|
14400
|
+
*/
|
|
14401
|
+
async archiveOldShipped(projectId) {
|
|
14402
|
+
const data = await this.read(projectId);
|
|
14403
|
+
const threshold = getDaysAgo(ARCHIVE_POLICIES.SHIPPED_RETENTION_DAYS);
|
|
14404
|
+
const stale = data.shipped.filter((s) => new Date(s.shippedAt) < threshold);
|
|
14405
|
+
if (stale.length === 0) return 0;
|
|
14406
|
+
archiveStorage.archiveMany(
|
|
14407
|
+
projectId,
|
|
14408
|
+
stale.map((s) => ({
|
|
14409
|
+
entityType: "shipped",
|
|
14410
|
+
entityId: s.id,
|
|
14411
|
+
entityData: s,
|
|
14412
|
+
summary: `${s.name} v${s.version}`,
|
|
14413
|
+
reason: "age"
|
|
14414
|
+
}))
|
|
14415
|
+
);
|
|
14416
|
+
const freshIds = new Set(
|
|
14417
|
+
data.shipped.filter((s) => new Date(s.shippedAt) >= threshold).map((s) => s.id)
|
|
14418
|
+
);
|
|
14419
|
+
await this.update(projectId, (d) => ({
|
|
14420
|
+
shipped: d.shipped.filter((s) => freshIds.has(s.id)),
|
|
14421
|
+
lastUpdated: getTimestamp()
|
|
14422
|
+
}));
|
|
14423
|
+
await this.publishEvent(projectId, "shipped.archived", {
|
|
14424
|
+
count: stale.length,
|
|
14425
|
+
oldestShippedAt: stale[stale.length - 1]?.shippedAt
|
|
14426
|
+
});
|
|
14427
|
+
return stale.length;
|
|
14428
|
+
}
|
|
14157
14429
|
};
|
|
14158
14430
|
shippedStorage = new ShippedStorage();
|
|
14159
14431
|
}
|
|
@@ -14557,6 +14829,7 @@ var init_state_storage = __esm({
|
|
|
14557
14829
|
init_date_helper();
|
|
14558
14830
|
init_markdown_builder();
|
|
14559
14831
|
init_state_machine();
|
|
14832
|
+
init_archive_storage();
|
|
14560
14833
|
init_storage_manager();
|
|
14561
14834
|
StateStorage = class extends StorageManager {
|
|
14562
14835
|
static {
|
|
@@ -14809,8 +15082,9 @@ var init_state_storage = __esm({
|
|
|
14809
15082
|
return pausedTasks.filter((t) => new Date(t.pausedAt).getTime() < threshold);
|
|
14810
15083
|
}
|
|
14811
15084
|
/**
|
|
14812
|
-
* Archive stale paused tasks (
|
|
14813
|
-
*
|
|
15085
|
+
* Archive stale paused tasks (PRJ-267).
|
|
15086
|
+
* Persists to archive table before removing from active storage.
|
|
15087
|
+
* Returns archived tasks.
|
|
14814
15088
|
*/
|
|
14815
15089
|
async archiveStalePausedTasks(projectId) {
|
|
14816
15090
|
const state = await this.read(projectId);
|
|
@@ -14819,6 +15093,16 @@ var init_state_storage = __esm({
|
|
|
14819
15093
|
const stale = pausedTasks.filter((t) => new Date(t.pausedAt).getTime() < threshold);
|
|
14820
15094
|
const fresh = pausedTasks.filter((t) => new Date(t.pausedAt).getTime() >= threshold);
|
|
14821
15095
|
if (stale.length === 0) return [];
|
|
15096
|
+
archiveStorage.archiveMany(
|
|
15097
|
+
projectId,
|
|
15098
|
+
stale.map((task) => ({
|
|
15099
|
+
entityType: "paused_task",
|
|
15100
|
+
entityId: task.id,
|
|
15101
|
+
entityData: task,
|
|
15102
|
+
summary: task.description,
|
|
15103
|
+
reason: "staleness"
|
|
15104
|
+
}))
|
|
15105
|
+
);
|
|
14822
15106
|
await this.update(projectId, (s) => ({
|
|
14823
15107
|
...s,
|
|
14824
15108
|
pausedTasks: fresh,
|
|
@@ -15434,6 +15718,7 @@ var init_storage2 = __esm({
|
|
|
15434
15718
|
"core/storage/index.ts"() {
|
|
15435
15719
|
"use strict";
|
|
15436
15720
|
init_analysis_storage();
|
|
15721
|
+
init_archive_storage();
|
|
15437
15722
|
init_database();
|
|
15438
15723
|
init_ideas_storage();
|
|
15439
15724
|
init_index_storage();
|
|
@@ -18572,6 +18857,211 @@ When fragmenting tasks:
|
|
|
18572
18857
|
}
|
|
18573
18858
|
});
|
|
18574
18859
|
|
|
18860
|
+
// core/utils/retry.ts
|
|
18861
|
+
function isTransientError(error) {
|
|
18862
|
+
if (!error || typeof error !== "object") {
|
|
18863
|
+
return false;
|
|
18864
|
+
}
|
|
18865
|
+
const err = error;
|
|
18866
|
+
if (err.code && TRANSIENT_ERROR_CODES.has(err.code)) {
|
|
18867
|
+
return true;
|
|
18868
|
+
}
|
|
18869
|
+
if (err.code && PERMANENT_ERROR_CODES.has(err.code)) {
|
|
18870
|
+
return false;
|
|
18871
|
+
}
|
|
18872
|
+
if (err.message) {
|
|
18873
|
+
const msg = err.message.toLowerCase();
|
|
18874
|
+
if (msg.includes("timeout") || msg.includes("timed out")) {
|
|
18875
|
+
return true;
|
|
18876
|
+
}
|
|
18877
|
+
}
|
|
18878
|
+
return false;
|
|
18879
|
+
}
|
|
18880
|
+
function isPermanentError(error) {
|
|
18881
|
+
if (!error || typeof error !== "object") {
|
|
18882
|
+
return false;
|
|
18883
|
+
}
|
|
18884
|
+
const err = error;
|
|
18885
|
+
return !!(err.code && PERMANENT_ERROR_CODES.has(err.code));
|
|
18886
|
+
}
|
|
18887
|
+
function isCircuitOpen(operationId, threshold, timeoutMs) {
|
|
18888
|
+
const state = circuitStates.get(operationId);
|
|
18889
|
+
if (!state) {
|
|
18890
|
+
return false;
|
|
18891
|
+
}
|
|
18892
|
+
if (state.consecutiveFailures >= threshold && state.openedAt) {
|
|
18893
|
+
const elapsed = Date.now() - state.openedAt;
|
|
18894
|
+
if (elapsed >= timeoutMs) {
|
|
18895
|
+
circuitStates.delete(operationId);
|
|
18896
|
+
return false;
|
|
18897
|
+
}
|
|
18898
|
+
return true;
|
|
18899
|
+
}
|
|
18900
|
+
return false;
|
|
18901
|
+
}
|
|
18902
|
+
function recordFailure(operationId, threshold) {
|
|
18903
|
+
const state = circuitStates.get(operationId) || {
|
|
18904
|
+
consecutiveFailures: 0,
|
|
18905
|
+
openedAt: null
|
|
18906
|
+
};
|
|
18907
|
+
state.consecutiveFailures++;
|
|
18908
|
+
if (state.consecutiveFailures >= threshold && !state.openedAt) {
|
|
18909
|
+
state.openedAt = Date.now();
|
|
18910
|
+
}
|
|
18911
|
+
circuitStates.set(operationId, state);
|
|
18912
|
+
}
|
|
18913
|
+
function recordSuccess(operationId) {
|
|
18914
|
+
circuitStates.delete(operationId);
|
|
18915
|
+
}
|
|
18916
|
+
var TRANSIENT_ERROR_CODES, PERMANENT_ERROR_CODES, circuitStates, RetryPolicy, defaultAgentRetryPolicy, defaultToolRetryPolicy;
|
|
18917
|
+
var init_retry = __esm({
|
|
18918
|
+
"core/utils/retry.ts"() {
|
|
18919
|
+
"use strict";
|
|
18920
|
+
TRANSIENT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
18921
|
+
"EBUSY",
|
|
18922
|
+
// Resource busy
|
|
18923
|
+
"EAGAIN",
|
|
18924
|
+
// Resource temporarily unavailable
|
|
18925
|
+
"ETIMEDOUT",
|
|
18926
|
+
// Operation timed out
|
|
18927
|
+
"ECONNRESET",
|
|
18928
|
+
// Connection reset by peer
|
|
18929
|
+
"ECONNREFUSED",
|
|
18930
|
+
// Connection refused (may be temporary)
|
|
18931
|
+
"ENOTFOUND",
|
|
18932
|
+
// DNS lookup failed (may be temporary)
|
|
18933
|
+
"EAI_AGAIN"
|
|
18934
|
+
// DNS temporary failure
|
|
18935
|
+
]);
|
|
18936
|
+
PERMANENT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
18937
|
+
"ENOENT",
|
|
18938
|
+
// No such file or directory
|
|
18939
|
+
"EACCES",
|
|
18940
|
+
// Permission denied
|
|
18941
|
+
"EPERM",
|
|
18942
|
+
// Operation not permitted
|
|
18943
|
+
"EISDIR",
|
|
18944
|
+
// Is a directory
|
|
18945
|
+
"ENOTDIR",
|
|
18946
|
+
// Not a directory
|
|
18947
|
+
"EINVAL"
|
|
18948
|
+
// Invalid argument
|
|
18949
|
+
]);
|
|
18950
|
+
__name(isTransientError, "isTransientError");
|
|
18951
|
+
__name(isPermanentError, "isPermanentError");
|
|
18952
|
+
circuitStates = /* @__PURE__ */ new Map();
|
|
18953
|
+
__name(isCircuitOpen, "isCircuitOpen");
|
|
18954
|
+
__name(recordFailure, "recordFailure");
|
|
18955
|
+
__name(recordSuccess, "recordSuccess");
|
|
18956
|
+
RetryPolicy = class {
|
|
18957
|
+
static {
|
|
18958
|
+
__name(this, "RetryPolicy");
|
|
18959
|
+
}
|
|
18960
|
+
options;
|
|
18961
|
+
constructor(options = {}) {
|
|
18962
|
+
this.options = {
|
|
18963
|
+
maxAttempts: options.maxAttempts ?? 3,
|
|
18964
|
+
baseDelayMs: options.baseDelayMs ?? 1e3,
|
|
18965
|
+
maxDelayMs: options.maxDelayMs ?? 8e3,
|
|
18966
|
+
circuitBreakerThreshold: options.circuitBreakerThreshold ?? 5,
|
|
18967
|
+
circuitBreakerTimeoutMs: options.circuitBreakerTimeoutMs ?? 6e4
|
|
18968
|
+
};
|
|
18969
|
+
}
|
|
18970
|
+
/**
|
|
18971
|
+
* Execute an operation with retry logic
|
|
18972
|
+
*
|
|
18973
|
+
* @param operation - Async function to execute
|
|
18974
|
+
* @param operationId - Optional ID for circuit breaker tracking
|
|
18975
|
+
* @returns Result of the operation
|
|
18976
|
+
* @throws Error if all attempts fail or circuit is open
|
|
18977
|
+
*/
|
|
18978
|
+
async execute(operation, operationId = "default") {
|
|
18979
|
+
if (isCircuitOpen(
|
|
18980
|
+
operationId,
|
|
18981
|
+
this.options.circuitBreakerThreshold,
|
|
18982
|
+
this.options.circuitBreakerTimeoutMs
|
|
18983
|
+
)) {
|
|
18984
|
+
throw new Error(
|
|
18985
|
+
`Circuit breaker is open for operation: ${operationId}. Too many consecutive failures.`
|
|
18986
|
+
);
|
|
18987
|
+
}
|
|
18988
|
+
let lastError;
|
|
18989
|
+
let attempt = 0;
|
|
18990
|
+
while (attempt < this.options.maxAttempts) {
|
|
18991
|
+
try {
|
|
18992
|
+
const result = await operation();
|
|
18993
|
+
recordSuccess(operationId);
|
|
18994
|
+
return result;
|
|
18995
|
+
} catch (error) {
|
|
18996
|
+
lastError = error;
|
|
18997
|
+
attempt++;
|
|
18998
|
+
if (isPermanentError(error)) {
|
|
18999
|
+
recordFailure(operationId, this.options.circuitBreakerThreshold);
|
|
19000
|
+
throw error;
|
|
19001
|
+
}
|
|
19002
|
+
const shouldRetry = isTransientError(error) && attempt < this.options.maxAttempts;
|
|
19003
|
+
if (!shouldRetry) {
|
|
19004
|
+
recordFailure(operationId, this.options.circuitBreakerThreshold);
|
|
19005
|
+
throw error;
|
|
19006
|
+
}
|
|
19007
|
+
const delay = Math.min(
|
|
19008
|
+
this.options.baseDelayMs * 2 ** (attempt - 1),
|
|
19009
|
+
this.options.maxDelayMs
|
|
19010
|
+
);
|
|
19011
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
19012
|
+
}
|
|
19013
|
+
}
|
|
19014
|
+
recordFailure(operationId, this.options.circuitBreakerThreshold);
|
|
19015
|
+
throw lastError;
|
|
19016
|
+
}
|
|
19017
|
+
/**
|
|
19018
|
+
* Check if an error is transient (exposed for testing)
|
|
19019
|
+
*/
|
|
19020
|
+
isTransientError(error) {
|
|
19021
|
+
return isTransientError(error);
|
|
19022
|
+
}
|
|
19023
|
+
/**
|
|
19024
|
+
* Check if circuit is open for an operation (exposed for testing)
|
|
19025
|
+
*/
|
|
19026
|
+
isCircuitOpen(operationId) {
|
|
19027
|
+
return isCircuitOpen(
|
|
19028
|
+
operationId,
|
|
19029
|
+
this.options.circuitBreakerThreshold,
|
|
19030
|
+
this.options.circuitBreakerTimeoutMs
|
|
19031
|
+
);
|
|
19032
|
+
}
|
|
19033
|
+
/**
|
|
19034
|
+
* Get current circuit state for an operation (exposed for testing)
|
|
19035
|
+
*/
|
|
19036
|
+
getCircuitState(operationId) {
|
|
19037
|
+
return circuitStates.get(operationId);
|
|
19038
|
+
}
|
|
19039
|
+
/**
|
|
19040
|
+
* Reset circuit breaker for an operation (exposed for testing)
|
|
19041
|
+
*/
|
|
19042
|
+
resetCircuit(operationId) {
|
|
19043
|
+
circuitStates.delete(operationId);
|
|
19044
|
+
}
|
|
19045
|
+
/**
|
|
19046
|
+
* Reset all circuit breakers (exposed for testing)
|
|
19047
|
+
*/
|
|
19048
|
+
resetAllCircuits() {
|
|
19049
|
+
circuitStates.clear();
|
|
19050
|
+
}
|
|
19051
|
+
};
|
|
19052
|
+
defaultAgentRetryPolicy = new RetryPolicy({
|
|
19053
|
+
maxAttempts: 3,
|
|
19054
|
+
baseDelayMs: 1e3,
|
|
19055
|
+
maxDelayMs: 8e3
|
|
19056
|
+
});
|
|
19057
|
+
defaultToolRetryPolicy = new RetryPolicy({
|
|
19058
|
+
maxAttempts: 2,
|
|
19059
|
+
baseDelayMs: 500,
|
|
19060
|
+
maxDelayMs: 2e3
|
|
19061
|
+
});
|
|
19062
|
+
}
|
|
19063
|
+
});
|
|
19064
|
+
|
|
18575
19065
|
// core/agentic/tool-registry.ts
|
|
18576
19066
|
import { exec as exec7 } from "node:child_process";
|
|
18577
19067
|
import fs36 from "node:fs/promises";
|
|
@@ -18580,6 +19070,7 @@ var execAsync4, toolRegistry, tool_registry_default;
|
|
|
18580
19070
|
var init_tool_registry = __esm({
|
|
18581
19071
|
"core/agentic/tool-registry.ts"() {
|
|
18582
19072
|
"use strict";
|
|
19073
|
+
init_retry();
|
|
18583
19074
|
execAsync4 = promisify8(exec7);
|
|
18584
19075
|
toolRegistry = {
|
|
18585
19076
|
tools: /* @__PURE__ */ new Map(),
|
|
@@ -18618,16 +19109,34 @@ var init_tool_registry = __esm({
|
|
|
18618
19109
|
};
|
|
18619
19110
|
toolRegistry.register("Read", async (filePath) => {
|
|
18620
19111
|
try {
|
|
18621
|
-
return await
|
|
18622
|
-
|
|
19112
|
+
return await defaultToolRetryPolicy.execute(
|
|
19113
|
+
async () => await fs36.readFile(filePath, "utf-8"),
|
|
19114
|
+
`read-${filePath}`
|
|
19115
|
+
);
|
|
19116
|
+
} catch (error) {
|
|
19117
|
+
if (isPermanentError(error)) {
|
|
19118
|
+
return null;
|
|
19119
|
+
}
|
|
19120
|
+
if (isTransientError(error)) {
|
|
19121
|
+
return null;
|
|
19122
|
+
}
|
|
18623
19123
|
return null;
|
|
18624
19124
|
}
|
|
18625
19125
|
});
|
|
18626
19126
|
toolRegistry.register("Write", async (filePath, content) => {
|
|
18627
19127
|
try {
|
|
18628
|
-
await
|
|
19128
|
+
await defaultToolRetryPolicy.execute(
|
|
19129
|
+
async () => await fs36.writeFile(filePath, content, "utf-8"),
|
|
19130
|
+
`write-${filePath}`
|
|
19131
|
+
);
|
|
18629
19132
|
return true;
|
|
18630
|
-
} catch (
|
|
19133
|
+
} catch (error) {
|
|
19134
|
+
if (isPermanentError(error)) {
|
|
19135
|
+
return false;
|
|
19136
|
+
}
|
|
19137
|
+
if (isTransientError(error)) {
|
|
19138
|
+
return false;
|
|
19139
|
+
}
|
|
18631
19140
|
return false;
|
|
18632
19141
|
}
|
|
18633
19142
|
});
|
|
@@ -18635,8 +19144,10 @@ var init_tool_registry = __esm({
|
|
|
18635
19144
|
"Bash",
|
|
18636
19145
|
async (command) => {
|
|
18637
19146
|
try {
|
|
18638
|
-
|
|
18639
|
-
|
|
19147
|
+
return await defaultToolRetryPolicy.execute(
|
|
19148
|
+
async () => await execAsync4(command),
|
|
19149
|
+
`bash-${command}`
|
|
19150
|
+
);
|
|
18640
19151
|
} catch (error) {
|
|
18641
19152
|
const err = error;
|
|
18642
19153
|
return {
|
|
@@ -19279,6 +19790,7 @@ var init_agent_generator = __esm({
|
|
|
19279
19790
|
"core/services/agent-generator.ts"() {
|
|
19280
19791
|
"use strict";
|
|
19281
19792
|
init_preserve_sections();
|
|
19793
|
+
init_retry();
|
|
19282
19794
|
}
|
|
19283
19795
|
});
|
|
19284
19796
|
|
|
@@ -19507,6 +20019,7 @@ var init_agent_service = __esm({
|
|
|
19507
20019
|
init_agent_router();
|
|
19508
20020
|
init_errors();
|
|
19509
20021
|
init_agent_detector();
|
|
20022
|
+
init_retry();
|
|
19510
20023
|
init_();
|
|
19511
20024
|
VALID_AGENT_TYPES = ["claude"];
|
|
19512
20025
|
AgentService = class {
|
|
@@ -19521,20 +20034,23 @@ var init_agent_service = __esm({
|
|
|
19521
20034
|
}
|
|
19522
20035
|
/**
|
|
19523
20036
|
* Initialize agent (Claude Code, Desktop, or Terminal)
|
|
20037
|
+
* Wrapped with retry policy to handle transient failures
|
|
19524
20038
|
*/
|
|
19525
20039
|
async initialize() {
|
|
19526
20040
|
if (this.agent) return this.agent;
|
|
19527
|
-
|
|
19528
|
-
|
|
19529
|
-
|
|
19530
|
-
|
|
19531
|
-
|
|
19532
|
-
|
|
19533
|
-
|
|
19534
|
-
|
|
19535
|
-
|
|
19536
|
-
|
|
19537
|
-
|
|
20041
|
+
return await defaultAgentRetryPolicy.execute(async () => {
|
|
20042
|
+
this.agentInfo = await detect2();
|
|
20043
|
+
if (!this.agentInfo?.isSupported) {
|
|
20044
|
+
throw AgentError.notSupported(this.agentInfo?.type ?? "unknown");
|
|
20045
|
+
}
|
|
20046
|
+
const agentType = this.agentInfo.type;
|
|
20047
|
+
if (!agentType || !VALID_AGENT_TYPES.includes(agentType)) {
|
|
20048
|
+
throw AgentError.notSupported(this.agentInfo?.type ?? "unknown");
|
|
20049
|
+
}
|
|
20050
|
+
const { default: Agent } = await globImport_infrastructure_agent(`../infrastructure/${agentType}-agent`);
|
|
20051
|
+
this.agent = new Agent();
|
|
20052
|
+
return this.agent;
|
|
20053
|
+
}, "agent-initialization");
|
|
19538
20054
|
}
|
|
19539
20055
|
/**
|
|
19540
20056
|
* Get current agent info
|
|
@@ -20764,6 +21280,7 @@ var init_memory_service = __esm({
|
|
|
20764
21280
|
"use strict";
|
|
20765
21281
|
init_config_manager();
|
|
20766
21282
|
init_path_manager();
|
|
21283
|
+
init_archive_storage();
|
|
20767
21284
|
init_fs();
|
|
20768
21285
|
init_date_helper();
|
|
20769
21286
|
init_jsonl_helper();
|
|
@@ -20859,6 +21376,39 @@ var init_memory_service = __esm({
|
|
|
20859
21376
|
return [];
|
|
20860
21377
|
}
|
|
20861
21378
|
}
|
|
21379
|
+
/**
|
|
21380
|
+
* Cap memory log at max entries (PRJ-267).
|
|
21381
|
+
* Moves overflow entries to archive table, keeps most recent entries.
|
|
21382
|
+
* Returns count of archived entries.
|
|
21383
|
+
*/
|
|
21384
|
+
async capEntries(projectId) {
|
|
21385
|
+
try {
|
|
21386
|
+
const memoryPath = path_manager_default.getFilePath(projectId, "memory", "context.jsonl");
|
|
21387
|
+
const entries = await readJsonLines(memoryPath);
|
|
21388
|
+
if (entries.length <= ARCHIVE_POLICIES.MEMORY_MAX_ENTRIES) {
|
|
21389
|
+
return 0;
|
|
21390
|
+
}
|
|
21391
|
+
const overflow = entries.slice(0, entries.length - ARCHIVE_POLICIES.MEMORY_MAX_ENTRIES);
|
|
21392
|
+
const kept = entries.slice(-ARCHIVE_POLICIES.MEMORY_MAX_ENTRIES);
|
|
21393
|
+
archiveStorage.archiveMany(
|
|
21394
|
+
projectId,
|
|
21395
|
+
overflow.map((entry, i) => ({
|
|
21396
|
+
entityType: "memory_entry",
|
|
21397
|
+
entityId: `memory-${entry.timestamp || i}`,
|
|
21398
|
+
entityData: entry,
|
|
21399
|
+
summary: entry.action,
|
|
21400
|
+
reason: "overflow"
|
|
21401
|
+
}))
|
|
21402
|
+
);
|
|
21403
|
+
await writeJsonLines(memoryPath, kept);
|
|
21404
|
+
return overflow.length;
|
|
21405
|
+
} catch (error) {
|
|
21406
|
+
if (!isNotFoundError(error)) {
|
|
21407
|
+
console.error(`Memory cap error: ${getErrorMessage2(error)}`);
|
|
21408
|
+
}
|
|
21409
|
+
return 0;
|
|
21410
|
+
}
|
|
21411
|
+
}
|
|
20862
21412
|
};
|
|
20863
21413
|
memoryService = new MemoryService();
|
|
20864
21414
|
}
|
|
@@ -26977,13 +27527,19 @@ var init_sync_service = __esm({
|
|
|
26977
27527
|
init_config_manager();
|
|
26978
27528
|
init_path_manager();
|
|
26979
27529
|
init_analysis_storage();
|
|
27530
|
+
init_archive_storage();
|
|
27531
|
+
init_ideas_storage();
|
|
26980
27532
|
init_metrics_storage();
|
|
26981
27533
|
init_migrate_json();
|
|
27534
|
+
init_queue_storage();
|
|
27535
|
+
init_shipped_storage();
|
|
27536
|
+
init_state_storage();
|
|
26982
27537
|
init_citations();
|
|
26983
27538
|
init_date_helper();
|
|
26984
27539
|
init_logger();
|
|
26985
27540
|
init_context_generator();
|
|
26986
27541
|
init_local_state_generator();
|
|
27542
|
+
init_memory_service();
|
|
26987
27543
|
init_skill_installer();
|
|
26988
27544
|
init_stack_detector();
|
|
26989
27545
|
init_sync_verifier();
|
|
@@ -27159,6 +27715,7 @@ var init_sync_service = __esm({
|
|
|
27159
27715
|
]);
|
|
27160
27716
|
const duration = Date.now() - startTime;
|
|
27161
27717
|
const syncMetrics = await this.recordSyncMetrics(stats, contextFiles, agents, duration);
|
|
27718
|
+
await this.archiveStaleData();
|
|
27162
27719
|
await command_installer_default.installGlobalConfig();
|
|
27163
27720
|
await command_installer_default.syncCommands();
|
|
27164
27721
|
let verification;
|
|
@@ -27938,6 +28495,40 @@ You are the ${name} expert for this project. Apply best practices for the detect
|
|
|
27938
28495
|
}
|
|
27939
28496
|
}
|
|
27940
28497
|
// ==========================================================================
|
|
28498
|
+
// ARCHIVAL (PRJ-267)
|
|
28499
|
+
// ==========================================================================
|
|
28500
|
+
/**
|
|
28501
|
+
* Archive stale data across all storage types.
|
|
28502
|
+
* Runs during sync to keep active storage lean.
|
|
28503
|
+
*/
|
|
28504
|
+
async archiveStaleData() {
|
|
28505
|
+
if (!this.projectId) return;
|
|
28506
|
+
try {
|
|
28507
|
+
const [shipped, dormant, staleQueue, stalePaused, memoryCapped] = await Promise.all([
|
|
28508
|
+
shippedStorage.archiveOldShipped(this.projectId).catch(() => 0),
|
|
28509
|
+
ideasStorage.markDormantIdeas(this.projectId).catch(() => 0),
|
|
28510
|
+
queueStorage.removeStaleCompleted(this.projectId).catch(() => 0),
|
|
28511
|
+
stateStorage.archiveStalePausedTasks(this.projectId).catch(() => []),
|
|
28512
|
+
memoryService.capEntries(this.projectId).catch(() => 0)
|
|
28513
|
+
]);
|
|
28514
|
+
const totalArchived = shipped + dormant + staleQueue + stalePaused.length + memoryCapped;
|
|
28515
|
+
if (totalArchived > 0) {
|
|
28516
|
+
logger_default.info("Archived stale data", {
|
|
28517
|
+
shipped,
|
|
28518
|
+
dormant,
|
|
28519
|
+
staleQueue,
|
|
28520
|
+
stalePaused: stalePaused.length,
|
|
28521
|
+
memoryCapped,
|
|
28522
|
+
total: totalArchived
|
|
28523
|
+
});
|
|
28524
|
+
const stats = archiveStorage.getStats(this.projectId);
|
|
28525
|
+
logger_default.debug("Archive stats", stats);
|
|
28526
|
+
}
|
|
28527
|
+
} catch (error) {
|
|
28528
|
+
logger_default.debug("Archival failed (non-critical)", { error: getErrorMessage(error) });
|
|
28529
|
+
}
|
|
28530
|
+
}
|
|
28531
|
+
// ==========================================================================
|
|
27941
28532
|
// HELPERS
|
|
27942
28533
|
// ==========================================================================
|
|
27943
28534
|
async fileExists(filename) {
|
|
@@ -33881,7 +34472,7 @@ var require_package = __commonJS({
|
|
|
33881
34472
|
"package.json"(exports, module) {
|
|
33882
34473
|
module.exports = {
|
|
33883
34474
|
name: "prjct-cli",
|
|
33884
|
-
version: "1.
|
|
34475
|
+
version: "1.20.0",
|
|
33885
34476
|
description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
|
|
33886
34477
|
main: "core/index.ts",
|
|
33887
34478
|
bin: {
|