fraim-hub 2.0.265 → 2.0.267

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.
@@ -62,8 +62,24 @@ const KNOWN_LABEL_OVERRIDES = {
62
62
  // shouldn't be split on dashes by the default humanizer.
63
63
  '1-1': '1:1',
64
64
  };
65
+ // Issue #1090: this reads JOB-LEVEL `## Intent` / `## Outcome` only.
66
+ //
67
+ // The previous expression was unanchored (`## ${heading}`), which had two
68
+ // consequences on a personalized override that customizes phases:
69
+ //
70
+ // 1. `#### Intent` CONTAINS the substring `## Intent`, so a phase-level heading
71
+ // satisfied a job-level query and the first phase's Intent was promoted to
72
+ // job-level metadata.
73
+ // 2. The terminator `\r?\n## ` requires a space as the 4th character, so it
74
+ // could not stop at `#### Steps` and Outcome swallowed the Steps and Skills
75
+ // blocks until `---` or end of file.
76
+ //
77
+ // The start anchor is written as `(?:^|\r?\n)` rather than using the `m` flag,
78
+ // because under `m` the trailing `$` would match end-of-LINE and truncate a
79
+ // multi-line Outcome to its first bullet. Terminating on any heading level
80
+ // `#{1,6} ` fixes (2).
65
81
  const sectionValue = (content, heading) => {
66
- const match = content.match(new RegExp(`## ${heading}\\r?\\n([\\s\\S]*?)(?:\\r?\\n## |\\r?\\n---|$)`));
82
+ const match = content.match(new RegExp(`(?:^|\\r?\\n)## ${heading}[ \\t]*\\r?\\n([\\s\\S]*?)(?:\\r?\\n#{1,6} |\\r?\\n---|$)`));
67
83
  if (!match)
68
84
  return [];
69
85
  return match[1]
@@ -72,6 +88,62 @@ const sectionValue = (content, heading) => {
72
88
  .filter(Boolean)
73
89
  .map((line) => line.replace(/^[-*]\s*/, '').trim());
74
90
  };
91
+ /**
92
+ * Issue #1090: resolve the baseline stub that an `extends`-only override inherits
93
+ * from, so job-level Intent/Outcome can be inherited rather than scavenged from
94
+ * the override's first phase.
95
+ *
96
+ * `extends` is written as `<category>/<job-name>`. Only NON-personalized layers
97
+ * are searched: an override extending another override is not a supported shape,
98
+ * and following one would risk a cycle.
99
+ */
100
+ function resolveExtendedStubPath(projectPath, extendsValue) {
101
+ const relative = toPosix(extendsValue).replace(/\.md$/i, '');
102
+ if (!relative || relative.includes('..'))
103
+ return null;
104
+ const candidateRoots = [
105
+ path_1.default.join(projectPath, 'fraim', 'ai-employee', 'jobs'),
106
+ path_1.default.join(projectPath, 'fraim', 'ai-manager', 'jobs'),
107
+ path_1.default.join(projectPath, 'registry', 'jobs', 'ai-employee'),
108
+ path_1.default.join(projectPath, 'registry', 'jobs', 'ai-manager'),
109
+ path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-employee', 'jobs'),
110
+ ];
111
+ for (const root of candidateRoots) {
112
+ const candidate = path_1.default.join(root, `${relative}.md`);
113
+ if (fs_1.default.existsSync(candidate))
114
+ return candidate;
115
+ }
116
+ return null;
117
+ }
118
+ /**
119
+ * Job-level Intent/Outcome for a stub, following `extends` when the stub itself
120
+ * declares none. Returns empty arrays rather than throwing when nothing resolves,
121
+ * so a broken `extends` degrades to the existing "No intent summary available."
122
+ * behaviour instead of breaking catalog discovery.
123
+ */
124
+ function jobLevelSections(content, filePath, projectPath) {
125
+ const intent = sectionValue(content, 'Intent');
126
+ const outcome = sectionValue(content, 'Outcome');
127
+ if (intent.length > 0 || outcome.length > 0)
128
+ return { intent, outcome };
129
+ const frontmatter = readJobFrontmatter(filePath);
130
+ const extendsValue = typeof frontmatter?.extends === 'string' ? frontmatter.extends.trim() : '';
131
+ if (!extendsValue)
132
+ return { intent, outcome };
133
+ const basePath = resolveExtendedStubPath(projectPath, extendsValue);
134
+ if (!basePath)
135
+ return { intent, outcome };
136
+ try {
137
+ const baseContent = fs_1.default.readFileSync(basePath, 'utf8');
138
+ return {
139
+ intent: sectionValue(baseContent, 'Intent'),
140
+ outcome: sectionValue(baseContent, 'Outcome'),
141
+ };
142
+ }
143
+ catch {
144
+ return { intent, outcome };
145
+ }
146
+ }
75
147
  const humanizeName = (raw) => {
76
148
  const lower = raw.toLowerCase();
77
149
  if (KNOWN_LABEL_OVERRIDES[lower])
@@ -122,8 +194,9 @@ function parseJobStub(filePath, categoryId, categoryLabel, projectPath, personal
122
194
  const fileName = path_1.default.basename(filePath, '.md');
123
195
  const frontmatter = readJobFrontmatter(filePath);
124
196
  const title = frontmatter?.displayName?.trim() || humanizeName(fileName);
125
- const intent = sectionValue(content, 'Intent')[0] || 'No intent summary available.';
126
- const outcome = sectionValue(content, 'Outcome');
197
+ const sections = jobLevelSections(content, filePath, projectPath);
198
+ const intent = sections.intent[0] || 'No intent summary available.';
199
+ const outcome = sections.outcome;
127
200
  return {
128
201
  id: fileName,
129
202
  title,
@@ -51,9 +51,11 @@ function withBucketLock(lockPath, fn, opts = {}) {
51
51
  fd = fs_1.default.openSync(lockPath, 'wx'); // O_CREAT | O_EXCL: fails if the lock already exists
52
52
  }
53
53
  catch (error) {
54
- if (error.code !== 'EEXIST')
54
+ const code = error.code;
55
+ const retryableContention = code === 'EEXIST' || code === 'EPERM' || code === 'EACCES';
56
+ if (!retryableContention)
55
57
  throw error;
56
- if (isLockStale(lockPath, staleMs)) {
58
+ if (fs_1.default.existsSync(lockPath) && isLockStale(lockPath, staleMs)) {
57
59
  try {
58
60
  fs_1.default.unlinkSync(lockPath);
59
61
  }
@@ -27,6 +27,7 @@ const HEADER_OMITTED_FIELDS = [
27
27
  'artifacts',
28
28
  'run',
29
29
  'delegation',
30
+ 'handoffSummary',
30
31
  '_bodyLoaded',
31
32
  '_stopping',
32
33
  '_priorEvents',
@@ -89,6 +89,7 @@ class HostSessionState {
89
89
  if (conversation.sessionId === normalizedSessionId) {
90
90
  const replacement = this.resolve({ ...conversation, sessionId: null }, owner);
91
91
  conversation.sessionId = replacement?.sessionId || null;
92
+ conversation.resumeCommand = null;
92
93
  }
93
94
  }
94
95
  markInvalidRun(run, owner, sessionId, reason, at = new Date().toISOString()) {
@@ -110,6 +111,7 @@ class HostSessionState {
110
111
  };
111
112
  if (run.sessionId === normalizedSessionId) {
112
113
  run.sessionId = undefined;
114
+ run.resumeCommand = null;
113
115
  }
114
116
  }
115
117
  hasInvalidSession(conversation, owner, sessionId) {
@@ -10,6 +10,7 @@ exports.parseUsageSignal = parseUsageSignal;
10
10
  exports.parseAgentIdentitySignal = parseAgentIdentitySignal;
11
11
  exports.__setAgentAvailabilityPathForTests = __setAgentAvailabilityPathForTests;
12
12
  exports.invalidateEmployeeDetectionCache = invalidateEmployeeDetectionCache;
13
+ exports.__clearEmployeeDetectionMemoryCacheForTests = __clearEmployeeDetectionMemoryCacheForTests;
13
14
  exports.__setEmployeeDetectionTtlForTests = __setEmployeeDetectionTtlForTests;
14
15
  exports.__getEmployeeProbeRoundsForTests = __getEmployeeProbeRoundsForTests;
15
16
  exports.__resetEmployeeProbeRoundsForTests = __resetEmployeeProbeRoundsForTests;
@@ -940,6 +941,13 @@ function invalidateEmployeeDetectionCache() {
940
941
  }
941
942
  catch { /* best effort */ }
942
943
  }
944
+ /** Test seam only. Simulates a process restart while preserving persisted last-known data. */
945
+ function __clearEmployeeDetectionMemoryCacheForTests() {
946
+ cachedEmployees = null;
947
+ cachedEmployeesAtMs = 0;
948
+ cachedEmployeesContext = null;
949
+ inFlightDetection = null;
950
+ }
943
951
  /** Test seam only. Mirrors __resetLatestVersionCache in hub-latest-version.ts. */
944
952
  function __setEmployeeDetectionTtlForTests(ttlMs) {
945
953
  employeeDetectionTtlMs = ttlMs ?? EMPLOYEE_DETECTION_TTL_MS;
@@ -1493,6 +1501,9 @@ function parseHostLine(hostId, line) {
1493
1501
  if (hostId === 'codex') {
1494
1502
  try {
1495
1503
  const parsed = JSON.parse(trimmed);
1504
+ if (parsed.type === 'system' && parsed.subtype === 'status' && parsed.status === 'compacting') {
1505
+ return withSignal({ raw: trimmed, hostLifecycle: { status: 'compacting', source: hostId } });
1506
+ }
1496
1507
  if (parsed.type === 'thread.started' && parsed.thread_id) {
1497
1508
  return withSignal({ sessionId: parsed.thread_id, raw: trimmed });
1498
1509
  }