@gethmy/agent 1.20.1 → 1.21.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.
Files changed (3) hide show
  1. package/dist/cli.js +107 -51
  2. package/dist/index.js +107 -51
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -3240,12 +3240,48 @@ ${summary}`,
3240
3240
  agent_identifier: "harmony-agent"
3241
3241
  };
3242
3242
  }
3243
- async function writeEpisode(client, input) {
3243
+ async function writeEpisode(client, input, options) {
3244
3244
  const payload = buildEpisodePayload(input, input.card.project_id);
3245
+ let content = payload.content;
3246
+ if (options?.distiller) {
3247
+ try {
3248
+ const distilled = await options.distiller(input, payload.content);
3249
+ if (distilled && distilled.trim().length > 0) {
3250
+ content = distilled.trim();
3251
+ }
3252
+ } catch (err) {
3253
+ log.warn(TAG10, `episode distillation failed for #${input.card.short_id}`, {
3254
+ cardId: input.card.id,
3255
+ event: "episode_distill_failed",
3256
+ kind: input.kind,
3257
+ error: err instanceof Error ? err.message : String(err)
3258
+ });
3259
+ }
3260
+ }
3261
+ const metadata = payload.metadata;
3245
3262
  try {
3263
+ const existingId = await findRollingEpisode(client, input.workspaceId, input.card.project_id, input.card.short_id, input.kind);
3264
+ if (existingId) {
3265
+ await client.updateMemoryEntity(existingId, {
3266
+ title: payload.title,
3267
+ content,
3268
+ metadata,
3269
+ confidence: payload.confidence,
3270
+ tags: payload.tags,
3271
+ type: payload.type
3272
+ });
3273
+ log.info(TAG10, `episode rolled for #${input.card.short_id}`, {
3274
+ cardId: input.card.id,
3275
+ event: "episode_rolled",
3276
+ kind: input.kind,
3277
+ entityId: existingId
3278
+ });
3279
+ return existingId;
3280
+ }
3246
3281
  const { entity } = await client.createMemoryEntity({
3247
3282
  ...payload,
3248
- metadata: payload.metadata
3283
+ content,
3284
+ metadata
3249
3285
  });
3250
3286
  const id = entity && typeof entity === "object" && "id" in entity ? entity.id ?? null : null;
3251
3287
  log.info(TAG10, `episode written for #${input.card.short_id}`, {
@@ -3264,31 +3300,44 @@ async function writeEpisode(client, input) {
3264
3300
  return null;
3265
3301
  }
3266
3302
  }
3267
- async function findLatestImplementEpisode(client, workspaceId, projectId, cardShortId) {
3303
+ async function findRollingEpisode(client, workspaceId, projectId, cardShortId, kind) {
3304
+ const type = kind === "implement" ? ["solution", "error"] : ["decision"];
3268
3305
  try {
3269
3306
  const { entities } = await client.harmonyRecall({
3270
3307
  workspaceId,
3271
3308
  projectId,
3272
- type: ["solution", "error"],
3309
+ type,
3273
3310
  memory_tier: "episode",
3274
3311
  scope: "project",
3275
3312
  tags: [`card:${cardShortId}`],
3276
- topK: 1
3313
+ topK: 10,
3314
+ includeEpisodes: true
3277
3315
  });
3278
- const first = entities[0];
3279
- if (first && typeof first === "object" && "id" in first && typeof first.id === "string") {
3280
- return first.id;
3316
+ for (const entity of entities) {
3317
+ if (!entity || typeof entity !== "object")
3318
+ continue;
3319
+ const meta = entity.metadata;
3320
+ if (meta?.episode_kind !== kind || meta?.origin?.source !== "agent-run") {
3321
+ continue;
3322
+ }
3323
+ const id = entity.id;
3324
+ if (typeof id === "string")
3325
+ return id;
3281
3326
  }
3282
3327
  return null;
3283
3328
  } catch (err) {
3284
- log.warn(TAG10, "implement-episode lookup failed", {
3329
+ log.warn(TAG10, "rolling-episode lookup failed", {
3285
3330
  event: "episode_lookup_failed",
3286
3331
  cardShortId,
3332
+ kind,
3287
3333
  error: err instanceof Error ? err.message : String(err)
3288
3334
  });
3289
3335
  return null;
3290
3336
  }
3291
3337
  }
3338
+ async function findLatestImplementEpisode(client, workspaceId, projectId, cardShortId) {
3339
+ return findRollingEpisode(client, workspaceId, projectId, cardShortId, "implement");
3340
+ }
3292
3341
  async function backfillReviewVerdict(client, originalEpisodeId, verdict, reviewEpisodeId) {
3293
3342
  try {
3294
3343
  if (verdict === "approved") {
@@ -8937,53 +8986,58 @@ class Pool {
8937
8986
  }
8938
8987
  }
8939
8988
  async enqueue(card, column, labels, subtasks, mode = "implement") {
8940
- if (this.implQueue.has(card.id) || this.reviewQueue.has(card.id) || this.isCardActive(card.id)) {
8941
- log.debug(TAG31, `Card ${card.id} already queued or active, skipping`);
8989
+ if (this.isCardKnown(card.id) || this.reservations.has(card.id)) {
8990
+ log.debug(TAG31, `Card ${card.id} already queued, active, or reserved, skipping`);
8942
8991
  return;
8943
8992
  }
8944
- if (mode === "implement") {
8945
- if (this.authPaused) {
8946
- log.debug(TAG31, `#${card.short_id} held — agent paused (auth error)`);
8947
- await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
8948
- return;
8949
- }
8950
- const cooldownMs = this.apiCooldownRemainingMs();
8951
- if (cooldownMs > 0) {
8952
- log.debug(TAG31, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
8953
- await this.emitWaiting(card.id, `Paused Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
8954
- return;
8955
- }
8956
- const decision = this.budget.check(card.id);
8957
- if (!decision.allow) {
8958
- if (decision.reason === "daily_budget") {
8959
- log.warn(TAG31, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
8960
- await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
8961
- } else {
8962
- log.debug(TAG31, `#${card.short_id} gave up: ${decision.detail}`);
8993
+ this.reservations.add(card.id);
8994
+ try {
8995
+ if (mode === "implement") {
8996
+ if (this.authPaused) {
8997
+ log.debug(TAG31, `#${card.short_id} held — agent paused (auth error)`);
8998
+ await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
8999
+ return;
9000
+ }
9001
+ const cooldownMs = this.apiCooldownRemainingMs();
9002
+ if (cooldownMs > 0) {
9003
+ log.debug(TAG31, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
9004
+ await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
9005
+ return;
9006
+ }
9007
+ const decision = this.budget.check(card.id);
9008
+ if (!decision.allow) {
9009
+ if (decision.reason === "daily_budget") {
9010
+ log.warn(TAG31, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
9011
+ await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
9012
+ } else {
9013
+ log.debug(TAG31, `#${card.short_id} gave up: ${decision.detail}`);
9014
+ }
9015
+ return;
9016
+ }
9017
+ const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
9018
+ if (blockers === null) {
9019
+ log.warn(TAG31, `#${card.short_id} blocker check failed — deferring to next tick`);
9020
+ return;
9021
+ }
9022
+ if (blockers.length > 0) {
9023
+ const list = blockers.map((b) => `#${b.shortId}`).join(", ");
9024
+ log.info(TAG31, `#${card.short_id} blocked by ${list} — waiting`);
9025
+ await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
9026
+ return;
8963
9027
  }
8964
- return;
8965
- }
8966
- const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
8967
- if (blockers === null) {
8968
- log.warn(TAG31, `#${card.short_id} blocker check failed — deferring to next tick`);
8969
- return;
8970
9028
  }
8971
- if (blockers.length > 0) {
8972
- const list = blockers.map((b) => `#${b.shortId}`).join(", ");
8973
- log.info(TAG31, `#${card.short_id} blocked by ${list} waiting`);
8974
- await this.emitWaiting(card.id, `Blocked by ${list} waiting for chain`);
8975
- return;
9029
+ const queue = mode === "review" ? this.reviewQueue : this.implQueue;
9030
+ queue.enqueue(card, column, labels, mode);
9031
+ this.cardDataCache.set(card.id, { card, column, labels, subtasks, mode });
9032
+ const workers = mode === "review" ? this.reviewWorkers : this.implWorkers;
9033
+ const dispatched = this.tryDispatchFor(workers, queue, mode);
9034
+ if (!dispatched) {
9035
+ const position = queue.cardIds().indexOf(card.id) + 1;
9036
+ const total = queue.length;
9037
+ await this.emitWaiting(card.id, position > 0 ? `Queued (${position}/${total}) — waiting for ${mode} worker` : `Queued — waiting for ${mode} worker`);
8976
9038
  }
8977
- }
8978
- const queue = mode === "review" ? this.reviewQueue : this.implQueue;
8979
- queue.enqueue(card, column, labels, mode);
8980
- this.cardDataCache.set(card.id, { card, column, labels, subtasks, mode });
8981
- const workers = mode === "review" ? this.reviewWorkers : this.implWorkers;
8982
- const dispatched = this.tryDispatchFor(workers, queue, mode);
8983
- if (!dispatched) {
8984
- const position = queue.cardIds().indexOf(card.id) + 1;
8985
- const total = queue.length;
8986
- await this.emitWaiting(card.id, position > 0 ? `Queued (${position}/${total}) — waiting for ${mode} worker` : `Queued — waiting for ${mode} worker`);
9039
+ } finally {
9040
+ this.reservations.delete(card.id);
8987
9041
  }
8988
9042
  }
8989
9043
  lastWaitingEmit = new Map;
@@ -9025,6 +9079,7 @@ class Pool {
9025
9079
  async removeCard(cardId) {
9026
9080
  await this.stateStore.resetAttempts(cardId);
9027
9081
  this.lastWaitingEmit.delete(cardId);
9082
+ this.reservations.delete(cardId);
9028
9083
  for (const queue of [this.implQueue, this.reviewQueue]) {
9029
9084
  const removed = queue.remove(cardId);
9030
9085
  if (removed) {
@@ -9129,6 +9184,7 @@ class Pool {
9129
9184
  this.sleepGuard.stop();
9130
9185
  log.info(TAG31, "Pool shutdown complete");
9131
9186
  }
9187
+ reservations = new Set;
9132
9188
  cardDataCache = new Map;
9133
9189
  tryDispatchFor(workers, queue, label) {
9134
9190
  if (this.shuttingDown)
package/dist/index.js CHANGED
@@ -3239,12 +3239,48 @@ ${summary}`,
3239
3239
  agent_identifier: "harmony-agent"
3240
3240
  };
3241
3241
  }
3242
- async function writeEpisode(client, input) {
3242
+ async function writeEpisode(client, input, options) {
3243
3243
  const payload = buildEpisodePayload(input, input.card.project_id);
3244
+ let content = payload.content;
3245
+ if (options?.distiller) {
3246
+ try {
3247
+ const distilled = await options.distiller(input, payload.content);
3248
+ if (distilled && distilled.trim().length > 0) {
3249
+ content = distilled.trim();
3250
+ }
3251
+ } catch (err) {
3252
+ log.warn(TAG10, `episode distillation failed for #${input.card.short_id}`, {
3253
+ cardId: input.card.id,
3254
+ event: "episode_distill_failed",
3255
+ kind: input.kind,
3256
+ error: err instanceof Error ? err.message : String(err)
3257
+ });
3258
+ }
3259
+ }
3260
+ const metadata = payload.metadata;
3244
3261
  try {
3262
+ const existingId = await findRollingEpisode(client, input.workspaceId, input.card.project_id, input.card.short_id, input.kind);
3263
+ if (existingId) {
3264
+ await client.updateMemoryEntity(existingId, {
3265
+ title: payload.title,
3266
+ content,
3267
+ metadata,
3268
+ confidence: payload.confidence,
3269
+ tags: payload.tags,
3270
+ type: payload.type
3271
+ });
3272
+ log.info(TAG10, `episode rolled for #${input.card.short_id}`, {
3273
+ cardId: input.card.id,
3274
+ event: "episode_rolled",
3275
+ kind: input.kind,
3276
+ entityId: existingId
3277
+ });
3278
+ return existingId;
3279
+ }
3245
3280
  const { entity } = await client.createMemoryEntity({
3246
3281
  ...payload,
3247
- metadata: payload.metadata
3282
+ content,
3283
+ metadata
3248
3284
  });
3249
3285
  const id = entity && typeof entity === "object" && "id" in entity ? entity.id ?? null : null;
3250
3286
  log.info(TAG10, `episode written for #${input.card.short_id}`, {
@@ -3263,31 +3299,44 @@ async function writeEpisode(client, input) {
3263
3299
  return null;
3264
3300
  }
3265
3301
  }
3266
- async function findLatestImplementEpisode(client, workspaceId, projectId, cardShortId) {
3302
+ async function findRollingEpisode(client, workspaceId, projectId, cardShortId, kind) {
3303
+ const type = kind === "implement" ? ["solution", "error"] : ["decision"];
3267
3304
  try {
3268
3305
  const { entities } = await client.harmonyRecall({
3269
3306
  workspaceId,
3270
3307
  projectId,
3271
- type: ["solution", "error"],
3308
+ type,
3272
3309
  memory_tier: "episode",
3273
3310
  scope: "project",
3274
3311
  tags: [`card:${cardShortId}`],
3275
- topK: 1
3312
+ topK: 10,
3313
+ includeEpisodes: true
3276
3314
  });
3277
- const first = entities[0];
3278
- if (first && typeof first === "object" && "id" in first && typeof first.id === "string") {
3279
- return first.id;
3315
+ for (const entity of entities) {
3316
+ if (!entity || typeof entity !== "object")
3317
+ continue;
3318
+ const meta = entity.metadata;
3319
+ if (meta?.episode_kind !== kind || meta?.origin?.source !== "agent-run") {
3320
+ continue;
3321
+ }
3322
+ const id = entity.id;
3323
+ if (typeof id === "string")
3324
+ return id;
3280
3325
  }
3281
3326
  return null;
3282
3327
  } catch (err) {
3283
- log.warn(TAG10, "implement-episode lookup failed", {
3328
+ log.warn(TAG10, "rolling-episode lookup failed", {
3284
3329
  event: "episode_lookup_failed",
3285
3330
  cardShortId,
3331
+ kind,
3286
3332
  error: err instanceof Error ? err.message : String(err)
3287
3333
  });
3288
3334
  return null;
3289
3335
  }
3290
3336
  }
3337
+ async function findLatestImplementEpisode(client, workspaceId, projectId, cardShortId) {
3338
+ return findRollingEpisode(client, workspaceId, projectId, cardShortId, "implement");
3339
+ }
3291
3340
  async function backfillReviewVerdict(client, originalEpisodeId, verdict, reviewEpisodeId) {
3292
3341
  try {
3293
3342
  if (verdict === "approved") {
@@ -8936,53 +8985,58 @@ class Pool {
8936
8985
  }
8937
8986
  }
8938
8987
  async enqueue(card, column, labels, subtasks, mode = "implement") {
8939
- if (this.implQueue.has(card.id) || this.reviewQueue.has(card.id) || this.isCardActive(card.id)) {
8940
- log.debug(TAG31, `Card ${card.id} already queued or active, skipping`);
8988
+ if (this.isCardKnown(card.id) || this.reservations.has(card.id)) {
8989
+ log.debug(TAG31, `Card ${card.id} already queued, active, or reserved, skipping`);
8941
8990
  return;
8942
8991
  }
8943
- if (mode === "implement") {
8944
- if (this.authPaused) {
8945
- log.debug(TAG31, `#${card.short_id} held — agent paused (auth error)`);
8946
- await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
8947
- return;
8948
- }
8949
- const cooldownMs = this.apiCooldownRemainingMs();
8950
- if (cooldownMs > 0) {
8951
- log.debug(TAG31, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
8952
- await this.emitWaiting(card.id, `Paused Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
8953
- return;
8954
- }
8955
- const decision = this.budget.check(card.id);
8956
- if (!decision.allow) {
8957
- if (decision.reason === "daily_budget") {
8958
- log.warn(TAG31, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
8959
- await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
8960
- } else {
8961
- log.debug(TAG31, `#${card.short_id} gave up: ${decision.detail}`);
8992
+ this.reservations.add(card.id);
8993
+ try {
8994
+ if (mode === "implement") {
8995
+ if (this.authPaused) {
8996
+ log.debug(TAG31, `#${card.short_id} held — agent paused (auth error)`);
8997
+ await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
8998
+ return;
8999
+ }
9000
+ const cooldownMs = this.apiCooldownRemainingMs();
9001
+ if (cooldownMs > 0) {
9002
+ log.debug(TAG31, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
9003
+ await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
9004
+ return;
9005
+ }
9006
+ const decision = this.budget.check(card.id);
9007
+ if (!decision.allow) {
9008
+ if (decision.reason === "daily_budget") {
9009
+ log.warn(TAG31, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
9010
+ await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
9011
+ } else {
9012
+ log.debug(TAG31, `#${card.short_id} gave up: ${decision.detail}`);
9013
+ }
9014
+ return;
9015
+ }
9016
+ const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
9017
+ if (blockers === null) {
9018
+ log.warn(TAG31, `#${card.short_id} blocker check failed — deferring to next tick`);
9019
+ return;
9020
+ }
9021
+ if (blockers.length > 0) {
9022
+ const list = blockers.map((b) => `#${b.shortId}`).join(", ");
9023
+ log.info(TAG31, `#${card.short_id} blocked by ${list} — waiting`);
9024
+ await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
9025
+ return;
8962
9026
  }
8963
- return;
8964
- }
8965
- const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
8966
- if (blockers === null) {
8967
- log.warn(TAG31, `#${card.short_id} blocker check failed — deferring to next tick`);
8968
- return;
8969
9027
  }
8970
- if (blockers.length > 0) {
8971
- const list = blockers.map((b) => `#${b.shortId}`).join(", ");
8972
- log.info(TAG31, `#${card.short_id} blocked by ${list} waiting`);
8973
- await this.emitWaiting(card.id, `Blocked by ${list} waiting for chain`);
8974
- return;
9028
+ const queue = mode === "review" ? this.reviewQueue : this.implQueue;
9029
+ queue.enqueue(card, column, labels, mode);
9030
+ this.cardDataCache.set(card.id, { card, column, labels, subtasks, mode });
9031
+ const workers = mode === "review" ? this.reviewWorkers : this.implWorkers;
9032
+ const dispatched = this.tryDispatchFor(workers, queue, mode);
9033
+ if (!dispatched) {
9034
+ const position = queue.cardIds().indexOf(card.id) + 1;
9035
+ const total = queue.length;
9036
+ await this.emitWaiting(card.id, position > 0 ? `Queued (${position}/${total}) — waiting for ${mode} worker` : `Queued — waiting for ${mode} worker`);
8975
9037
  }
8976
- }
8977
- const queue = mode === "review" ? this.reviewQueue : this.implQueue;
8978
- queue.enqueue(card, column, labels, mode);
8979
- this.cardDataCache.set(card.id, { card, column, labels, subtasks, mode });
8980
- const workers = mode === "review" ? this.reviewWorkers : this.implWorkers;
8981
- const dispatched = this.tryDispatchFor(workers, queue, mode);
8982
- if (!dispatched) {
8983
- const position = queue.cardIds().indexOf(card.id) + 1;
8984
- const total = queue.length;
8985
- await this.emitWaiting(card.id, position > 0 ? `Queued (${position}/${total}) — waiting for ${mode} worker` : `Queued — waiting for ${mode} worker`);
9038
+ } finally {
9039
+ this.reservations.delete(card.id);
8986
9040
  }
8987
9041
  }
8988
9042
  lastWaitingEmit = new Map;
@@ -9024,6 +9078,7 @@ class Pool {
9024
9078
  async removeCard(cardId) {
9025
9079
  await this.stateStore.resetAttempts(cardId);
9026
9080
  this.lastWaitingEmit.delete(cardId);
9081
+ this.reservations.delete(cardId);
9027
9082
  for (const queue of [this.implQueue, this.reviewQueue]) {
9028
9083
  const removed = queue.remove(cardId);
9029
9084
  if (removed) {
@@ -9128,6 +9183,7 @@ class Pool {
9128
9183
  this.sleepGuard.stop();
9129
9184
  log.info(TAG31, "Pool shutdown complete");
9130
9185
  }
9186
+ reservations = new Set;
9131
9187
  cardDataCache = new Map;
9132
9188
  tryDispatchFor(workers, queue, label) {
9133
9189
  if (this.shuttingDown)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.20.1",
3
+ "version": "1.21.0",
4
4
  "description": "Push-based agent daemon for Harmony — watches board assignments and spawns Claude CLI workers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -46,7 +46,7 @@
46
46
  "dependencies": {
47
47
  "@anthropic-ai/claude-agent-sdk": "^0.3.178",
48
48
  "@supabase/supabase-js": "2.95.3",
49
- "@gethmy/mcp": "^2.16.0"
49
+ "@gethmy/mcp": "^2.17.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@harmony/shared": "workspace:*",